解決回調地獄的異步操做,Async 函數是終極辦法,但瞭解生成器和 Promise 有助於理解 Async 函數原理。因爲內容較多,分三部分進行,這是第三部分,介紹 Async 函數相關。第一部分介紹 Generator,第二部分介紹 Promise。ios
在這部分中,咱們會先介紹 Async 函數的基本使用,而後會結合前兩部分介紹的生成器和 Promise 實現一個 async 函數。git
經過在普通函數前加async
操做符能夠定義 Async 函數:github
// 這是一個 async 函數
async function() {}
複製代碼
Async 函數體中的代碼是異步執行的,不會阻塞後面代碼執行,但它們的寫法和同步代碼類似。ajax
Async 函數會 返回一個已完成的 promise 對象,實際在使用的時候會和await
操做符配合使用,在介紹await
以前,咱們先看看 async 函數自己有哪些特色。express
若是 async 函數體內若是沒有await
操做符,那麼它返回的 promise 對象狀態和他的函數體內代碼怎麼寫有關係,具體和 promise 的then()
方法的處理方式相同:axios
1)沒有顯式 return 任何數據segmentfault
此時默認返回Promise.resolve()
:promise
var a = (async () => {})();
複製代碼
至關於網絡
var a = (async () => {
return Promise.resolve();
})();
複製代碼
此時 a 的值:異步
a {
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: undefined
}
複製代碼
2)顯式 return 非 promise
至關於返回Promise.resolve(data)
var a = (async () => {
return 111;
})();
複製代碼
至關於
var a = (async () => {
return Promise.resolve(111);
})();
複製代碼
此時 a 的值:
a {
[[PromiseStatus]]: 'resolved',
[[PromiseValue]]: 111
}
複製代碼
3)顯式 return promise 對象
此時 async 函數返回的 promise 對象狀態由顯示返回的 promise 對象狀態決定,這裏以被拒絕的 promise 爲例:
var a = (async () => Promise.reject(111))();
複製代碼
此時 a 的值:
a {
[[PromiseStatus]]: 'rejected',
[[PromiseValue]]: 111
}
複製代碼
但實際使用中,咱們不會向上面那樣使用,而是配合await
操做符一塊兒使用,否則像上面那樣,和 promise 相比,並無優點可言。特別的,沒有await
操做符,咱們並不能用 async 函數解決相互依賴的異步數據的請求問題。
換句話說:咱們不關心 async 返回的 promise 狀態(一般狀況,async 函數不會返回任何內容,即默認返回Promise.resolve()
),咱們關心的是 async 函數體內的代碼怎麼寫,由於裏面的代碼能夠異步執行且不阻塞 async 函數後面代碼的執行,這就爲寫異步代碼創造了條件,而且書寫形式上和同步代碼同樣。
await
操做符使用方式以下:
[rv] = await expression;
複製代碼
expression:能夠是任何值,但一般是一個 promise;
rv: 可選。若是有且 expression 是非 promise 的值,則 rv 等於 expression 自己;否則,rv 等於 兌現 的 promise 的值,若是該 promise 被拒絕,則拋個異常(因此await
通常被 try-catch 包裹,異常能夠被捕獲到)。
但注意await
必須在 async 函數中使用,否則會報語法錯誤。
看下面代碼例子:
1)expression 後爲非 promise
(async () => {
const b = await 111;
console.log(b); // 111
})();
複製代碼
直接返回這個 expression 的值,即,打印 111
。
2)expression 爲兌現的 promise
(async () => {
const b = await Promise.resolve(111);
console.log(b); // 111
})();
複製代碼
返回兌現的 promise 的值,因此打印111
。
3)expression 爲拒絕的 promise
(async () => {
try {
const b = await Promise.reject(111);
// 前面的 await 出錯後,當前代碼塊後面的代碼就不執行了
console.log(b); // 不執行
} catch (e) {
console.log("出錯了:", e); // 出錯了:111
}
})();
複製代碼
若是await
後面的 promise 被拒絕或自己代碼執行出錯都會拋出一個異常,而後被 catch 到,而且,和當前await
同屬一個代碼塊的後面的代碼再也不執行。
在 promise 中咱們處理相互依賴的異步數據使用鏈式調用的方式,雖然相比回調函數已經優化不少,但書寫及理解上仍是沒有同步代碼直觀。咱們看下 async 函數如何解決這個問題。
先回顧下需求及 promise 的解決方案:
需求:請求 URL1 獲得 data1;請求 URL2 獲得 data2,但 URL2 = data1[0].url2;請求 URL3 獲得 data3,但 URL3 = data2[0].url3。
使用 promise 鏈式調用能夠這樣寫代碼:
promiseAjax 在 第二部分介紹 promise 時在 3.1 中定義的,經過 promise 封裝的 ajax GET 請求。
promiseAjax('URL1')
.then(data1 => promiseAjax(data1[0].url2))
.then(data2 => promiseAjax(data2[0].url3);)
.then(console.log(data3))
.catch(e => console.log(e));
複製代碼
若是使用 Async 函數則能夠像同步代碼的同樣寫:
async function() {
try {
const data1 = await promiseAjax('URL1');
const data2 = await promiseAjax(data1[0].url);
const data3 = await promiseAjax(data2[0].url);
} catch (e) {
console.log(e);
}
}
複製代碼
之因此能夠這樣用,是由於只有當前await
等待的 promise 兌現後,它後面的代碼纔會執行(或者拋出錯誤,後面代碼都不執行,直接去到 catch 分支)。
這裏有兩點值得關注:
1)await
幫咱們處理了 promise,要麼返回兌現的值,要麼拋出異常; 2)await
在等待 promise 兌現的同時,整個 async 函數會掛起,promise 兌現後再從新執行接下來的代碼。
對於第 2 點,是否是想到了生成器?在 1.4 節中咱們會經過生成器 + promise 本身寫一個 async 函數。
Async 函數沒有Promise.all()
之類的方法,咱們須要寫多幾個 async 函數。
能夠藉助Promise.all()
在同一個 async 函數中並行處理多個無依賴關係的異步數據,以下:
async function fn1() {
try {
const arr = await Promise.all([
promiseAjax("URL1"),
promiseAjax("URL2"),
]);
// ... do something
} catch (e) {
console.log(e);
}
}
複製代碼
但實際開發中若是異步請求的數據是業務不相關的,不推薦這樣寫,緣由以下:
把全部的異步請求放在一個 async 函數中至關於手動增強了業務代碼的耦合,會致使下面兩個問題:
1)寫代碼及獲取數據都不直觀,尤爲請求多起來的時候; 2)Promise.all
裏面寫多個無依賴的異步請求,若是 其中一個被拒絕或發生異常,全部請求的結果咱們都獲取不到。
若是業務場景是不關心上面兩點,能夠考慮使用上面的寫法,否則,每一個異步請求都放在不一樣的 async 函數中發出。
下面是分開寫的例子:
async function fn1() {
try {
const data1 = await promiseAjax("URL1");
// ... do something
} catch (e) {
console.log(e);
}
}
async function fn2() {
try {
const data2 = await promiseAjax("URL2");
// ... do something
} catch (e) {
console.log(e);
}
}
複製代碼
咱們先看下 async 處理異步的原理:
await
操做符會掛起;await
後面的表達式求值(一般是個耗時的異步操做)前 async 函數一直處於掛起狀態,避免阻塞 async 函數後面的代碼;await
後面的表達式求值求值後(異步操做完成),await
能夠對該值作處理:若是是非 promise,直接返回該值;若是是 promsie,則提取 promise 的值並返回。同時告訴 async 函數接着執行下面的代碼;await
後面的那個異步操做,每每是返回 promise 對象(好比 axios),而後交給 await
處理,畢竟,async-await 的設計初衷就是爲了解決異步請求數據時的回調地獄問題,而使用 promise 是關鍵一步。
async 函數自己的行爲,和生成器相似;而await
等待的一般是 promise 對象,也正因如此,常說 async 函數是 生成器 + promise 結合後的語法糖。
既然咱們知道了 async 函數處理異步數據的原理,接下來咱們就簡單模擬下 async 函數的實現過程。
這裏只模擬 async 函數配合await
處理網絡請求的場景,而且請求最終返回 promise 對象,async 函數自己返回值(已完成的 promise 對象)及更多使用場景這裏沒作考慮。
因此接下來的 myAsync 函數只是爲了說明 async-await 原理,不要將其用在生產環境中。
/** * 模擬 async 函數的實現,該段代碼取自 Secrets of the JavaScript Ninja (Second Edition),p159 */
// 接收生成器做爲參數,建議先移到後面,看下生成器中的代碼
var myAsync = generator => {
// 注意 iterator.next() 返回對象的 value 是 promiseAjax(),一個 promise
const iterator = generator();
// handle 函數控制 async 函數的 掛起-執行
const handle = iteratorResult => {
if (iteratorResult.done) return;
const iteratorValue = iteratorResult.value;
// 只考慮異步請求返回值是 promise 的狀況
if (iteratorValue instanceof Promise) {
// 遞歸調用 handle,promise 兌現後再調用 iterator.next() 使生成器繼續執行
// ps.原書then最後少了半個括號 ')'
iteratorValue
.then(result => handle(iterator.next(result)))
.catch(e => iterator.throw(e));
}
};
try {
handle(iterator.next());
} catch (e) {
console.log(e);
}
};
複製代碼
myAsync
接收的一個生成器做爲入參,生成器函數內部的代碼,和寫原生 async 函數相似,只是用yield
代替了await
myAsync(function*() {
try {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 10);
const c = yield Promise.resolve(b + 100);
console.log(a, b, c); // 輸出 1,11,111
} catch (e) {
console.log("出錯了:", e);
}
});
複製代碼
上面會打印1 11 111
。
若是第二個yield
語句後的 promise 被拒絕Promise.reject(a + 10)
,則打印出錯了:11
。
handle
函數,控制生成器的 掛起-執行。具體過程以下:
1)首先調用generator()
生成它的控制器,即迭代器iterator
,此時,生成器處於掛起狀態; 2)第一次調用handle
函數,並傳入iterator.next()
,這樣就完成生成器的第一次調用的; 3)執行生成器,遇到yield
生成器再次掛起,同時把yield
後表達式的結果(未完成的 promise)傳給 handle; 4)生成器掛起的同時,異步請求還在進行,異步請求完成(promise 兌現)後,會調用handle
函數中的iteratorValue.then()
; 5)iteratorValue.then()
執行時內部遞歸調用handle
,同時把異步請求回的數據傳給生成器(iterator.next(result)
),生成器更新數據再次執行。若是出錯直接結束; 6)三、四、5 步重複執行,直到生成器結束,即iteratorResult.done === true
,myAsync 結束調用。
【1】[美]JOHN RESIG,BEAR BIBEAULT and JOSIP MARAS 著(2016),Secrets of the JavaScript Ninja (Second Edition),p159,Manning Publications Co.
【2】async function-MDN
【3】await-MDN
【4】理解 JavaScript 的 async/await