[轉] 理解 JavaScript 的 async/await

[From] https://segmentfault.com/a/1190000007535316      邊城 2016年11月19日發佈javascript

 

隨着 Node 7 的發佈,愈來愈多的人開始研究聽說是異步編程終級解決方案的 async/await。我第一次看到這組關鍵字並非在 JavaScript 語言裏,而是在 c# 5.0 的語法中。C# 的 async/await 須要在 .NET Framework 4.5 以上的版本中使用,所以我還很悲傷了一陣——爲了要兼容 XP 系統,咱們開發的軟件不能使用高於 4.0 版本的 .NET Framework。html

我以前在《閒談異步調用「扁平」化》 中就談到了這個問題。不管是在 C# 仍是 JavaScript 中,async/await 都是很是棒的特性,它們也都是很是甜的語法糖。C# 的 async/await 實現離不開 Task 或 Task<Result> 類,而 JavaScript 的 async/await 實現,也離不開 Promisejava

如今拋開 C# 和 .NET Framework,專心研究下 JavaScript 的 async/await。node

async 和 await 在幹什麼

任意一個名稱都是有意義的,先從字面意思來理解。async 是「異步」的簡寫,而 await 能夠認爲是 async wait 的簡寫。因此應該很好理解 async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。編程

另外還有一個頗有意思的語法規定,await 只能出如今 async 函數中。而後細心的朋友會產生一個疑問,若是 await 只能出如今 async 函數中,那這個 async 函數應該怎麼調用?c#

若是須要經過 await 來調用一個 async 函數,那這個調用的外面必須得再包一個 async 函數,而後……進入死循環,永無出頭之日……segmentfault

若是 async 函數不須要 await 來調用,那 async 到底起個啥做用?bash

async 起什麼做用

這個問題的關鍵在於,async 函數是怎麼處理它的返回值的!異步

咱們固然但願它能直接經過 return 語句返回咱們想要的值,可是若是真是這樣,彷佛就沒 await 什麼事了。因此,寫段代碼來試試,看它到底會返回什麼:async

async function testAsync() { return "hello async"; } const result = testAsync(); console.log(result);

看到輸出就恍然大悟了——輸出的是一個 Promise 對象。

c:\var\test> node --harmony_async_await . Promise { 'hello async' }

因此,async 函數返回的是一個 Promise 對象。從文檔中也能夠獲得這個信息。async 函數(包含函數語句、函數表達式、Lambda表達式)會返回一個 Promise 對象,若是在函數中 return 一個直接量,async 會把這個直接量經過 Promise.resolve() 封裝成 Promise 對象。

async 函數返回的是一個 Promise 對象,因此在最外層不能用 await 獲取其返回值的狀況下,咱們固然應該用原來的方式:then() 鏈來處理這個 Promise 對象,就像這樣

testAsync().then(v => { console.log(v); // 輸出 hello async });

如今回過頭來想下,若是 async 函數沒有返回值,又該如何?很容易想到,它會返回 Promise.resolve(undefined)

聯想一下 Promise 的特色——無等待,因此在沒有 await 的狀況下執行 async 函數,它會當即執行,返回一個 Promise 對象,而且,毫不會阻塞後面的語句。這和普通返回 Promise 對象的函數並沒有二致。

那麼下一個關鍵點就在於 await 關鍵字了。

await 到底在等啥

通常來講,都認爲 await 是在等待一個 async 函數完成。不過按語法說明,await 等待的是一個表達式,這個表達式的計算結果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定)。

由於 async 函數返回一個 Promise 對象,因此 await 能夠用於等待一個 async 函數的返回值——這也能夠說是 await 在等 async 函數,但要清楚,它等的實際是一個返回值。注意到 await 不只僅用於等 Promise 對象,它能夠等任意表達式的結果,因此,await 後面實際是能夠接普通函數調用或者直接量的。因此下面這個示例徹底能夠正確運行

function getSomething() { return "something"; } async function testAsync() { return Promise.resolve("hello async"); } async function test() { const v1 = await getSomething(); const v2 = await testAsync(); console.log(v1, v2); } test();

await 等到了要等的,而後呢

await 等到了它要等的東西,一個 Promise 對象,或者其它值,而後呢?我不得不先說,await 是個運算符,用於組成表達式,await 表達式的運算結果取決於它等的東西。

若是它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。

若是它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的代碼,等着 Promise 對象 resolve,而後獲得 resolve 的值,做爲 await 表達式的運算結果。

看到上面的阻塞一詞,心慌了吧……放心,這就是 await 必須用在 async 函數中的緣由。async 函數調用不會形成阻塞,它內部全部的阻塞都被封裝在一個 Promise 對象中異步執行。

async/await 幫咱們幹了啥

做個簡單的比較

上面已經說明了 async 會將其後的函數(函數表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,並將其 resolve 的結果返回出來。

如今舉例,用 setTimeout 模擬耗時的異步操做,先來看看不用 async/await 會怎麼寫

function takeLongTime() { return new Promise(resolve => { setTimeout(() => resolve("long_time_value"), 1000); }); } takeLongTime().then(v => { console.log("got", v); });

若是改用 async/await 呢,會是這樣

function takeLongTime() { return new Promise(resolve => { setTimeout(() => resolve("long_time_value"), 1000); }); } async function test() { const v = await takeLongTime(); console.log(v); } test();

眼尖的同窗已經發現 takeLongTime() 沒有申明爲 async。實際上,takeLongTime() 自己就是返回的 Promise 對象,加不加 async 結果都同樣,若是沒明白,請回過頭再去看看上面的「async 起什麼做用」。

又一個疑問產生了,這兩段代碼,兩種方式對異步調用的處理(實際就是對 Promise 對象的處理)差異並不明顯,甚至使用 async/await 還須要多寫一些代碼,那它的優點到底在哪?

async/await 的優點在於處理 then 鏈

單一的 Promise 鏈並不能發現 async/await 的優點,可是,若是須要處理由多個 Promise 組成的 then 鏈的時候,優點就能體現出來了(頗有意思,Promise 經過 then 鏈來解決多層回調的問題,如今又用 async/await 來進一步優化它)。

假設一個業務,分多個步驟完成,每一個步驟都是異步的,並且依賴於上一個步驟的結果。咱們仍然用 setTimeout 來模擬異步操做:

/** * 傳入參數 n,表示這個函數執行的時間(毫秒) * 執行的結果是 n + 200,這個值將用於下一步驟 */ function takeLongTime(n) { return new Promise(resolve => { setTimeout(() => resolve(n + 200), n); }); } function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n); } function step2(n) { console.log(`step2 with ${n}`); return takeLongTime(n); } function step3(n) { console.log(`step3 with ${n}`); return takeLongTime(n); }

如今用 Promise 方式來實現這三個步驟的處理

function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => step2(time2)) .then(time3 => step3(time3)) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); }); } doIt(); // c:\var\test>node --harmony_async_await . // step1 with 300 // step2 with 500 // step3 with 700 // result is 900 // doIt: 1507.251ms

輸出結果 result 是 step3() 的參數 700 + 200 = 900doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一致。

若是用 async/await 來實現呢,會是這樣

async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time2); const result = await step3(time3); console.log(`result is ${result}`); console.timeEnd("doIt"); } doIt();

結果和以前的 Promise 實現是同樣的,可是這個代碼看起來是否是清晰得多,幾乎跟同步代碼同樣

還有更酷的

如今把業務要求改一下,仍然是三個步驟,但每個步驟都須要以前每一個步驟的結果。

function step1(n) { console.log(`step1 with ${n}`); return takeLongTime(n); } function step2(m, n) { console.log(`step2 with ${m} and ${n}`); return takeLongTime(m + n); } function step3(k, m, n) { console.log(`step3 with ${k}, ${m} and ${n}`); return takeLongTime(k + m + n); }

這回先用 async/await 來寫:

async function doIt() { console.time("doIt"); const time1 = 300; const time2 = await step1(time1); const time3 = await step2(time1, time2); const result = await step3(time1, time2, time3); console.log(`result is ${result}`); console.timeEnd("doIt"); } doIt(); // c:\var\test>node --harmony_async_await . // step1 with 300 // step2 with 800 = 300 + 500 // step3 with 1800 = 300 + 500 + 1000 // result is 2000 // doIt: 2907.387ms

除了以爲執行時間變長了以外,彷佛和以前的示例沒啥區別啊!別急,認真想一想若是把它寫成 Promise 方式實現會是什麼樣子?

function doIt() { console.time("doIt"); const time1 = 300; step1(time1) .then(time2 => { return step2(time1, time2) .then(time3 => [time1, time2, time3]); }) .then(times => { const [time1, time2, time3] = times; return step3(time1, time2, time3); }) .then(result => { console.log(`result is ${result}`); console.timeEnd("doIt"); }); } doIt();

有沒有感受有點複雜的樣子?那一堆參數處理,就是 Promise 方案的死穴—— 參數傳遞太麻煩了,看着就暈!

洗洗睡吧

就目前來講,已經理解 async/await 了吧?但其實還有一些事情沒說起——Promise 有可能 reject 啊,怎麼處理呢?若是須要並行處理3個步驟,再等待全部結果,又該怎麼處理呢?

阮一峯老師已經說過了,我就懶得說了。

相關文章
相關標籤/搜索