解決回撥地獄的非同步操作,Async 函式是終極辦法,但瞭解生成器和 Promise 有助於理解 Async 函式原理。由於內容較多,分三部分進行,這是第三部分,介紹 Async 函式相關。第一部分介紹 Generator,第二部分介紹 Promise。
在這部分中,我們會先介紹 Async 函式的基本使用,然後會結合前兩部分介紹的生成器和 Promise 實現一個 async 函式。
1)Async 函式概覽
1.1 概念
通過在普通函式前加async
操作符可以定義 Async 函式:
// 這是一個 async 函式
async function() {}
複製程式碼
Async 函式體中的程式碼是非同步執行的,不會阻塞後面程式碼執行,但它們的寫法和同步程式碼相似。
Async 函式會 返回一個已完成的 promise 物件,實際在使用的時候會和await
操作符配合使用,在介紹await
之前,我們先看看 async 函式本身有哪些特點。
1.2 Async 函式基本用法
1.2.1 函式體內沒有 await
如果 async 函式體內如果沒有await
操作符,那麼它返回的 promise 物件狀態和他的函式體內程式碼怎麼寫有關係,具體和 promise 的then()
方法的處理方式相同:
1)沒有顯式 return 任何資料
此時預設返回Promise.resolve()
:
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 函式後面程式碼的執行,這就為寫非同步程式碼創造了條件,並且書寫形式上和同步程式碼一樣。
1.2.2 await 介紹
await
操作符使用方式如下:
[rv] = await expression;
複製程式碼
expression:可以是任何值,但通常是一個 promise;
rv: 可選。如果有且 expression 是非 promise 的值,則 rv 等於 expression 本身;不然,rv 等於 兌現 的 promise 的值,如果該 promise 被拒絕,則拋個異常(所以await
一般被 try-catch 包裹,異常可以被捕獲到)。
但注意await
必須在 async 函式中使用,不然會報語法錯誤。
1.2.3 await 使用
看下面程式碼例子:
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
同屬一個程式碼塊的後面的程式碼不再執行。
2)Async 函式處理非同步請求
2.1 相互依賴的非同步資料
在 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 函式。
2.2 無依賴關係的非同步資料
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);
}
}
複製程式碼
3)Async 模擬實現
3.1 async 函式處理非同步資料的原理
我們先看下 async 處理非同步的原理:
- async 函式遇到
await
操作符會掛起; await
後面的表示式求值(通常是個耗時的非同步操作)前 async 函式一直處於掛起狀態,避免阻塞 async 函式後面的程式碼;await
後面的表示式求值求值後(非同步操作完成),await
可以對該值做處理:如果是非 promise,直接返回該值;如果是 promsie,則提取 promise 的值並返回。同時告訴 async 函式接著執行下面的程式碼;- 哪裡出現異常,結束 async 函式。
await
後面的那個非同步操作,往往是返回 promise 物件(比如 axios),然後交給 await
處理,畢竟,async-await 的設計初衷就是為了解決非同步請求資料時的回撥地獄問題,而使用 promise 是關鍵一步。
async 函式本身的行為,和生成器類似;而await
等待的通常是 promise 物件,也正因如此,常說 async 函式是 生成器 + promise 結合後的語法糖。
既然我們知道了 async 函式處理非同步資料的原理,接下來我們就簡單模擬下 async 函式的實現過程。
3.2 async 函式簡單實現
這裡只模擬 async 函式配合await
處理網路請求的場景,並且請求最終返回 promise 物件,async 函式本身返回值(已完成的 promise 物件)及更多使用場景這裡沒做考慮。
所以接下來的 myAsync 函式只是為了說明 async-await 原理,不要將其用在生產環境中。
3.2.1 程式碼實現
/**
* 模擬 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);
}
};
複製程式碼
3.2.2 使用
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
。
3.2.3 說明:
- myAsync 函式接受一個生成器作為引數,控制生成器的 掛起 可達到使整個 myAsync 函式在非同步程式碼請求過程 掛起 的效果;
- myAsync 函式內部通過定義
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)3、4、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