ES6+ async/await
1. 前言
前面幾節(jié)我們已經(jīng)學(xué)習(xí)了解決異步的方案 Promise 和 Generator,上一節(jié)我們也通過一個案例使用 Promise + Generator
實現(xiàn)了一個比較好的異步解決方案。同時我們實現(xiàn)了一個簡版的 co 庫,讓我們在使用 Generator 函數(shù)處理異步任務(wù)時更加方便,但這不是最完美的解決方案。
本節(jié)我們將學(xué)習(xí) ES7 推出的 async/await
其特性是對 JS 的異步編程進行了重要的改進,在不阻塞主線程的情況下,它給我們提供了使用同步代碼的風(fēng)格來編寫異步任務(wù)的能力。另外,我們要明確的是 async/await
其實是 Promise + Generator
的語法糖,為了幫助我們像寫同步代碼一樣書寫異步代碼,代碼風(fēng)格更優(yōu)雅,錯誤捕獲也更容易。
本節(jié)我們將通過對上一節(jié)案例的改造。在不需要 co 庫的情況下直接使用 async/await
讓我們更加深刻地理解異步方案的演變過程。
2. 改造上節(jié)案例
上一節(jié) 我們通過一個案例來講解 Promise + Generator
在實際應(yīng)用中的使用,通過 Generator 函數(shù) 和 yield 讓異步代碼看起來像同步代碼一樣執(zhí)行。但是這樣里面存在的一個問題就是生成器函數(shù)直接執(zhí)行,需要手動處理。為了解決深層回調(diào)的問題我們借助了 co 庫來幫助我們?nèi)?zhí)行生成器函數(shù),從而解決了回調(diào)地獄的問題。下面是上一節(jié)的代碼。
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
是生成器函數(shù),不能直接調(diào)用,這里用 co 庫來進行執(zhí)行,然后通過 Promise 的鏈式調(diào)用獲取執(zhí)行后的結(jié)果。但是這里借助了 co 的庫,我們其實最希望的是能像執(zhí)行普通函數(shù)一樣直接調(diào)用 getValue
就能執(zhí)行并得到結(jié)果。 async/await
的出現(xiàn)就是為了抹平在調(diào)用時所做的額外步驟。那讓我們看看 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
函數(shù)和生成器函數(shù) */yield
定義的基本相同,但是在執(zhí)行時 async/await
定義的函數(shù)直接調(diào)用即可。從這里我們就能看到 async/await
的優(yōu)點,無需過多的操作非常優(yōu)雅和簡潔。
3. 用法
上面我們基本了解了 async 函數(shù),下面我們就來看看它的基本使用和需要注意的地方。
定義一個異步函數(shù)時需要使用 async
和 function
關(guān)鍵字一起來完成,類似生成器函數(shù)中的 yield
來暫停異步任務(wù),在 async 函數(shù)中使用 await
關(guān)鍵去等待異步任務(wù)返回的結(jié)果。
async 函數(shù)其本質(zhì)是 Promise + Generator
函數(shù)組成的語法糖,它為了減少了 Promise 的鏈式調(diào)用,解放了 Generator 函數(shù)的單步執(zhí)行。主要語法如下:
async function name([param[, param[, ... param]]]) {
statements
}
上面代碼中的 statements 是函數(shù)主體的表達式,async 函數(shù)可以通過 return
來返回一個值,這個返回值會被包裝成一個 Promise 實例,可以被鏈式調(diào)用。下面我們來看兩段等價代碼。
// 下面兩段代碼時相同的
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 函數(shù)是怎么實現(xiàn)的,其大概原理類似上節(jié)寫的 co 庫,有興趣的小伙伴可以去 babel 上去看看 async 函數(shù)編譯是什么樣子的。
當(dāng)在 async 函數(shù)中返回的是一個普通值或 await 后跟一個普通值時,此時的 async 函數(shù)是同步的。在 Promise 中失敗是不能被 try...catch
捕獲的,需要通過 catch
的方式來捕獲錯誤。而使用 async 函數(shù)則是可以通過 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()
上面的代碼中在執(zhí)行 foo()
直接拋出了一個錯誤,而 Promise 和 async/await 對錯誤的捕獲是不同的,我們知道 Promise 是通過 then
中的失敗回調(diào)和 catch
來捕獲錯誤的,而 async 函數(shù)使用的是 try...catch
更像同步的方式。
3.1 錯誤捕獲
但是有個問題,當(dāng)程序需要同時處理多個異步任務(wù)時,那我們使用 async/await
怎樣捕獲那個異步任務(wù)出現(xiàn)錯誤呢?try 塊中的代碼只要程序出現(xiàn)錯誤就會拋出錯誤,但是不知道是哪個異步任務(wù)出錯了不利于定位問題。如果使用多個 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 函數(shù)在異常捕獲時,沒有非常完美的解決方案,這主要源自依賴 try...catch
對錯誤的捕獲。但有一些還算比較優(yōu)雅的解決方案,我們已經(jīng)知道了 async 函數(shù)返回的是一個 Promise 那么我們是不是可以使用 Promise 的 catch
來捕獲呢?答案是當(dāng)然的呢。
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 函數(shù)之間的關(guān)系,不然就很難理解這種寫法。
3.2 濫用 async/await
既然 async/await
這么優(yōu)雅簡潔,那在編程的過程中都使用這個就好啦!其實這里是一個坑,很多時候 async/await
都會被濫用導(dǎo)致程序卡頓,執(zhí)行時間過長。
async function foo() {
let res1 = await task(100);
let res2 = await task(200);
let res3 = await task(300);
return { res1, res2, res3 }
}
foo()
在很多時候我們會寫成這樣的代碼,如果后一個任務(wù)依賴前一個任務(wù)這樣寫完全沒問題,但是如果是三個獨立的異步任務(wù),那這樣寫就會導(dǎo)致程序執(zhí)行時間加長。這樣的代碼過于同步化,我們需要牢記的是 await 看起來是同步的,但它仍然屬于異步的內(nèi)容,最終還是走的回調(diào),只是語言底層給我們做了很多工作。
針對沒有關(guān)聯(lián)的異步任務(wù)我們需要把它們解開,
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 外面去執(zhí)行呢?await 的本質(zhì)就是暫停異步任務(wù),等待返回結(jié)果,等到結(jié)果返回后就會繼續(xù)往下執(zhí)行。還要知道的是每個 task 都是一個異步任務(wù),像之前的那種寫法,await 會等待上一個異步任務(wù)完成才會走下一個。而我們把 task 拿出來了,也就是每個 task 會按照異步的方式去執(zhí)行。這個時候三個 task 都已經(jīng)開始執(zhí)行了,當(dāng)遇到 await 就只需要等到任務(wù)完成就行。所需要的時間是異步任務(wù)中耗時最長的,而不是之前的總和。
4. 小結(jié)
本節(jié)我們主要通過延續(xù)上一節(jié)的案例,用 async 函數(shù)給出了最優(yōu)的解決方案,從而完善了整個異步演變的過程,讓我們更加清晰地理解為什么會有 Promise?為什么會有生成器?為什么會有 async/await?由淺入深層層遞進地講解了 ES6 以后對異步任務(wù)處理的演變。然后我們主要學(xué)習(xí)了 async 函數(shù)的基本使用和錯誤處理的捕獲。最后,我們講解了如果不濫用 async 函數(shù)的案例,讓我們在以后寫程序的過程中更加得心應(yīng)手。