ES6+ async/await
1. 前言
前面幾節我們已經學習了解決異步的方案 Promise 和 Generator,上一節我們也通過一個案例使用 Promise + Generator
實現了一個比較好的異步解決方案。同時我們實現了一個簡版的 co 庫,讓我們在使用 Generator 函數處理異步任務時更加方便,但這不是最完美的解決方案。
本節我們將學習 ES7 推出的 async/await
其特性是對 JS 的異步編程進行了重要的改進,在不阻塞主線程的情況下,它給我們提供了使用同步代碼的風格來編寫異步任務的能力。另外,我們要明確的是 async/await
其實是 Promise + Generator
的語法糖,為了幫助我們像寫同步代碼一樣書寫異步代碼,代碼風格更優雅,錯誤捕獲也更容易。
本節我們將通過對上一節案例的改造。在不需要 co 庫的情況下直接使用 async/await
讓我們更加深刻地理解異步方案的演變過程。
2. 改造上節案例
上一節 我們通過一個案例來講解 Promise + Generator
在實際應用中的使用,通過 Generator 函數 和 yield 讓異步代碼看起來像同步代碼一樣執行。但是這樣里面存在的一個問題就是生成器函數直接執行,需要手動處理。為了解決深層回調的問題我們借助了 co 庫來幫助我們去執行生成器函數,從而解決了回調地獄的問題。下面是上一節的代碼。
const ajax = function(api) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (api === 'api_1') {
resolve('api_2');
}
if (api === 'api_2') {
resolve(100);
}
}, 0)
})
}
function * getValue() {
const api = yield ajax('api_1');
const value = yield ajax(api);
return value;
}
co(getValue()).then(res => {
console.log(res);
})
上面的代碼中 getValue
是生成器函數,不能直接調用,這里用 co 庫來進行執行,然后通過 Promise 的鏈式調用獲取執行后的結果。但是這里借助了 co 的庫,我們其實最希望的是能像執行普通函數一樣直接調用 getValue
就能執行并得到結果。 async/await
的出現就是為了抹平在調用時所做的額外步驟。那讓我們看看 async/await
是怎么用的:
async function getValue() {
const api = await ajax('api_1');
const value = await ajax(api);
console.log(value)
return value;
}
getValue() // 控制臺打印 value的值是:100
上面的代碼中我們可以看出使用 async/await
定義的 getValue
函數和生成器函數 */yield
定義的基本相同,但是在執行時 async/await
定義的函數直接調用即可。從這里我們就能看到 async/await
的優點,無需過多的操作非常優雅和簡潔。
3. 用法
上面我們基本了解了 async 函數,下面我們就來看看它的基本使用和需要注意的地方。
定義一個異步函數時需要使用 async
和 function
關鍵字一起來完成,類似生成器函數中的 yield
來暫停異步任務,在 async 函數中使用 await
關鍵去等待異步任務返回的結果。
async 函數其本質是 Promise + Generator
函數組成的語法糖,它為了減少了 Promise 的鏈式調用,解放了 Generator 函數的單步執行。主要語法如下:
async function name([param[, param[, ... param]]]) {
statements
}
上面代碼中的 statements 是函數主體的表達式,async 函數可以通過 return
來返回一個值,這個返回值會被包裝成一個 Promise 實例,可以被鏈式調用。下面我們來看兩段等價代碼。
// 下面兩段代碼時相同的
async function foo() {
return 100
}
function foo() {
return Promise.resolve(100)
}
// 下面兩段代碼時相同的
async function foo() {
await 1;
}
function foo() {
return Promise.resolve(1).then(() => undefined)
}
上面的兩段代碼效果時相同的,這里我們就不去探究 async 函數是怎么實現的,其大概原理類似上節寫的 co 庫,有興趣的小伙伴可以去 babel 上去看看 async 函數編譯是什么樣子的。
當在 async 函數中返回的是一個普通值或 await 后跟一個普通值時,此時的 async 函數是同步的。在 Promise 中失敗是不能被 try...catch
捕獲的,需要通過 catch
的方式來捕獲錯誤。而使用 async 函數則是可以通過 try...catch
來捕獲。
async function foo() {
return new Error('Throw an error');
}
foo().then(res => {
console.log(res)
}).catch(err => {
console.error(err) // Error: Throw an error
})
async function foo2() {
try{
var v = await foo()
console.log(v)
} catch(e) {
console.log(e); // Error: Throw an error
}
}
foo2()
上面的代碼中在執行 foo()
直接拋出了一個錯誤,而 Promise 和 async/await 對錯誤的捕獲是不同的,我們知道 Promise 是通過 then
中的失敗回調和 catch
來捕獲錯誤的,而 async 函數使用的是 try...catch
更像同步的方式。
3.1 錯誤捕獲
但是有個問題,當程序需要同時處理多個異步任務時,那我們使用 async/await
怎樣捕獲那個異步任務出現錯誤呢?try 塊中的代碼只要程序出現錯誤就會拋出錯誤,但是不知道是哪個異步任務出錯了不利于定位問題。如果使用多個 try...catch
:
const task = function (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (num === 300) {
reject('throw error')
} else {
resolve('imooc');
}
}, 1000)
})
}
async function foo() {
try {
let res1 = await task(100);
try {
let res2 = await task(200);
try {
let res3 = await task(300);
} catch(e) {
console.log('res3', e)
}
} catch(e) {
console.log('res2', e)
}
} catch(e) {
console.log('res1', e)
}
}
foo() // res3 throw error
看到上面的代碼你是不是覺得很難受啊,又回到了嵌套地獄的原始問題了。async 函數在異常捕獲時,沒有非常完美的解決方案,這主要源自依賴 try...catch
對錯誤的捕獲。但有一些還算比較優雅的解決方案,我們已經知道了 async 函數返回的是一個 Promise 那么我們是不是可以使用 Promise 的 catch
來捕獲呢?答案是當然的呢。
async function foo() {
let res1 = await task(100).catch(err => console.log('res1', err));
let res2 = await task(200).catch(err => console.log('res2', err));
let res3 = await task(300).catch(err => console.log('res3', err));
}
foo() // res3 throw error
上面的代碼看起來就比嵌套的 try...catch
感覺好很多,這也是一個比較好的解決方式。在使用 catch
時需要弄清楚 Promise 和 async 函數之間的關系,不然就很難理解這種寫法。
3.2 濫用 async/await
既然 async/await
這么優雅簡潔,那在編程的過程中都使用這個就好啦!其實這里是一個坑,很多時候 async/await
都會被濫用導致程序卡頓,執行時間過長。
async function foo() {
let res1 = await task(100);
let res2 = await task(200);
let res3 = await task(300);
return { res1, res2, res3 }
}
foo()
在很多時候我們會寫成這樣的代碼,如果后一個任務依賴前一個任務這樣寫完全沒問題,但是如果是三個獨立的異步任務,那這樣寫就會導致程序執行時間加長。這樣的代碼過于同步化,我們需要牢記的是 await 看起來是同步的,但它仍然屬于異步的內容,最終還是走的回調,只是語言底層給我們做了很多工作。
針對沒有關聯的異步任務我們需要把它們解開,
const task = function (num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('imooc ' + num);
}, 1000)
})
}
async function foo() {
let res1Promes = task(100);
let res2Promes = task(200);
let res3Promes = task(300);
let res1 = await res1Promes;
let res2 = await res2Promes;
let res3 = await res3Promes;
console.log({ res1, res2, res3 })
return { res1, res2, res3 }
}
foo(); // { res1: 'imooc 100', res2: 'imooc 200', res3: 'imooc 300' }
這里需要明白的一點是:為什么要把 task 拿到 await 外面去執行呢?await 的本質就是暫停異步任務,等待返回結果,等到結果返回后就會繼續往下執行。還要知道的是每個 task 都是一個異步任務,像之前的那種寫法,await 會等待上一個異步任務完成才會走下一個。而我們把 task 拿出來了,也就是每個 task 會按照異步的方式去執行。這個時候三個 task 都已經開始執行了,當遇到 await 就只需要等到任務完成就行。所需要的時間是異步任務中耗時最長的,而不是之前的總和。
4. 小結
本節我們主要通過延續上一節的案例,用 async 函數給出了最優的解決方案,從而完善了整個異步演變的過程,讓我們更加清晰地理解為什么會有 Promise?為什么會有生成器?為什么會有 async/await?由淺入深層層遞進地講解了 ES6 以后對異步任務處理的演變。然后我們主要學習了 async 函數的基本使用和錯誤處理的捕獲。最后,我們講解了如果不濫用 async 函數的案例,讓我們在以后寫程序的過程中更加得心應手。