重讀 ES6 - async+await 同步/非同步方案
非同步程式設計一直是JavaScript 程式設計的重大事項。關於非同步方案, ES6 先是出現了 基於狀態管理的 Promise,然後出現了 Generator 函式 + co 函式,緊接著又出現了 ES7 的 async + await 方案。
本文力求以最簡明的方式來疏通 async + await。
非同步程式設計的幾個場景
先從一個常見問題開始:一個for 迴圈中,如何非同步的列印迭代順序?
我們很容易想到用閉包,或者 ES6 規定的 let 塊級作用域來回答這個問題。
for (let val of [1, 2, 3, 4]) { setTimeout(() => console.log(val),100); } // => 預期結果依次為:1, 2, 3, 4
這裡描述的是一個均勻發生的的非同步,它們被依次按既定的順序排在非同步佇列中等待執行。
如果非同步不是均勻發生的,那麼它們被註冊在非同步佇列中的順序就是亂序的。
for (let val of [1, 2, 3, 4]) { setTimeout(() => console.log(val), 100 * Math.random()); } // => 實際結果是隨機的,依次為:4, 2, 3, 1
返回的結果是亂序不可控的,這本來就是最為真實的非同步。但另一種情況是,在迴圈中,如果希望前一個非同步執行完畢、後一個非同步再執行,該怎麼辦?
for (let val of ['a', 'b', 'c', 'd']) { // a 執行完後,進入下一個迴圈 // 執行 b,依此類推 }
這不就是多個非同步 “序列” 嗎!
在回撥 callback 巢狀非同步操作、再回撥的方式,不就解決了這個問題!或者,使用 Promise + then() 層層巢狀同樣也能解決問題。但是,如果硬是要將這種巢狀的方式寫在迴圈中,還恐怕還需費一番周折。試問,有更好的辦法嗎?
非同步同步化方案
試想,如果要去將一批資料傳送到伺服器,只有前一批傳送成功(即伺服器返回成功的響應),才開始下一批資料的傳送,否則終止傳送。這就是一個典型的 “for 迴圈中存在相互依賴的非同步操作” 的例子。
明顯,這種 “序列” 的非同步,實質上可以當成同步。它和亂序的非同步比較起來,花費了更多的時間。按理說,我們希望程式非同步執行,就是為了 “跳過” 阻塞,較少時間花銷。但與之相反的是,如果需要一系列的非同步 “序列”,我們應該怎樣很好的進行程式設計?
對於這個 “序列” 非同步,有了 ES6 就非常容易的解決了這個問題。
async function task () { for (let val of [1, 2, 3, 4]) { // await 是要等待響應的 let result = await send(val); if (!result) { break; } } } task();
從字面上看,就是本次迴圈,等有了結果,再進行下一次迴圈。因此,迴圈每執行一次就會被暫停(“卡住”)一次,直到迴圈結束。這種編碼實現,很好的消除了層層巢狀的 “回撥地獄” 問題,降低了認知難度。
這就是非同步問題同步化的方案。關於這個方案,如果說 Promise 主要解決的是非同步回撥問題,那麼 async + await 主要解決的就是將非同步問題同步化,降低非同步程式設計的認知負擔。
async + await “外異內同”
早先接觸這套 API 時,看著繁瑣的文件,一知半解的認為 async + await 主要用來解決非同步問題同步化的。
其實不然。從上面的例子看到:async 關鍵字宣告瞭一個 非同步函式,這個 非同步函式 體內有一行 await 語句,它告示了該行為同步執行,並且與上下相鄰的程式碼是依次逐行執行的。
將這個形式化的東西再翻譯一下,就是:
1、async 函式執行後,總是返回了一個 promise 物件
2、await 所在的那一行語句是同步的
其中,1 說明了從外部看,task 方法執行後返回一個 Promise 物件,正因為它返回的是 Promise,所以可以理解task 是一個非同步方法。毫無疑問它是這樣用的:
task().then((val) => {alert(val)}) .then((val) => {alert(val)})
2 說明了在 task 函式內部,非同步已經被 “削” 成了同步。整個就是一個執行稍微耗時的函式而已。
綜合 1、2,從形式上看,就是 “task 整體是一個非同步函式,內部整個是同步的”,簡稱“外異內同”。
整體是一個非同步函式 不難理解。在實現上,我們不妨逆向一下,語言層面讓async關鍵字呼叫時,在函式執行的末尾強制增加一個promise 反回:
async fn () { let result; // ... //末尾返回 promise return isPromise(result)? result : Promise.resolve(undefined); }
內部是同步的 是怎麼做到的?實際上 await 呼叫,是讓後邊的語句(函式)做了一個遞迴執行,直到獲取到結果並使其 狀態 變更,才會 resolve 掉,而只有 resolve 掉,await 那一行程式碼才算執行完,才繼續往下一行執行。所以,儘管外部是一個大大的 for 迴圈,但是整個 for 迴圈是依次序列的。
因此,僅從上述框架的外觀出發,就不難理解 async + await 的意義。使用起來也就這麼簡單,反而 Promise 是一個必須掌握的基礎件。
秉承本次《重讀 ES6》系列的原則,不過多追求理解細節和具體實現過程。我們繼續鞏固一下這個 “形式化” 的理解。
async + await 的進一步理解
有這樣的一個非同步操作 longTimeTask,已經用 Promise 進行了包裝。藉助該函式進行一系列驗證。
const longTimeTask = function (time) { return new Promise((resolve, reject) => { setTimeout(()=>{ console.log(`等了 ${time||'xx'} 年,終於回信了`); resolve({'msg': 'task done'}); }, time||1000) }) }
async 函式的執行情況
如果,想檢視 async exec1 函式的返回結果,以及 await 命令的執行結果:
const exec1 = async function () { let result = await longTimeTask(); console.log('result after long time ===>', result); } // 檢視函式內部執行順序 exec1(); // => 等了 xx 年,終於回信了 // => result after long time ===> Object {msg: "task done"} //檢視函式總體返回值 console.log(exec1()); // => Promise {[[PromiseStatus]]: "pending",...} // => 同上
以上 2 步執行,清晰的證明了 exec1 函式體內是同步、逐行逐行執行的,即先執行完非同步操作,然後進行 console.log() 列印。而 exec1() 的執行結果就直接是一個 Promise,因為它最先會蹦出來一串 Promise ...,然後才是 exec1 函式的內部執行日誌。
因此,所有驗證,完全符合 整體是一個非同步函式,內部整個是同步的 的總結。
await 如何執行其後語句?
回到 await ,看看它是如何執行其後邊的語句的。假設:讓 longTimeTask() 後邊直接帶 then() 回撥,分兩種情況:
1)then() 中不再返回任何東西
2) then() 中繼續手動返回另一個 promise
const exec2 = async function () { let result = await longTimeTask().then((res) => { console.log('then ===>', res.msg); res.msg = `${res.msg} then refrash message`; // 註釋掉這條 return 或 手動返回一個 promise return Promise.resolve(res); }); console.log('result after await ===>', result.msg); } exec2(); // => 情況一 TypeError: Cannot read property 'msg' of undefined // => 情況二 正常
首先,longTimeTask() 加上再多得 then() 回撥,也不過是放在了它的回撥列隊 queue 裡了。也就是說,await 命令之後始終是一條 表示式語句,只不過上述程式碼書寫方式比較讓人迷惑。(比較好的實踐建議是,將 longTimeTask 方法身後的 then() 移入 longTimeTask 函式體封裝起來)
其次,手動返回另一個 promise 和什麼也不返回,關係到 longTimeTask() 方法最終 resolve 出去的內容不一樣。換句話說,await 命令會提取其後邊的promise 的 resolve 結果,進而直接導致 result 的不同。
值得強調的是,await 命令只認 resolve 結果,對 reject 結果報錯。不妨用以下的 return 語句替換上述 return 進行驗證。
return Promise.reject(res);
最後
其實,關於非同步程式設計還有很多可以梳理的,比如跨模組的非同步程式設計、非同步的單元測試、非同步的錯誤處理以及什麼是好的實踐。All in all, 限於篇幅,不在此彙總了。最後,async + await 確實是一個很優雅的方案。
相關文章
- 一篇文章讀懂阻塞,非阻塞,同步,非同步非同步
- 同步非同步,阻塞非阻塞非同步
- 非同步、同步、阻塞、非阻塞非同步
- 非同步程式設計解決方案全集—promise、generator+co、async+await非同步程式設計PromiseAI
- 如何解讀 Java IO、NIO 中的同步阻塞與同步非阻塞?Java
- 同步、非同步,阻塞、非阻塞理解非同步
- 同步、非同步、阻塞與非阻塞非同步
- 同步非同步 與 阻塞非阻塞非同步
- 理解阻塞、非阻塞、同步、非同步非同步
- JS非同步之callback、promise、async+await簡介JS非同步PromiseAI
- 同步、非同步、阻塞、非阻塞的區別非同步
- ASP.Net中的async+await非同步程式設計ASP.NETAI非同步程式設計
- ES6 中的 三種非同步解決方案非同步
- IO - 同步 非同步 阻塞 非阻塞的區別非同步
- 徹底搞懂同步非同步與阻塞非阻塞非同步
- java同步非阻塞IOJava
- 同步阻塞、同步非阻塞、多路複用的介紹
- 大白話搞懂什麼是同步/非同步/阻塞/非阻塞非同步
- socket阻塞與非阻塞,同步與非同步、I/O模型非同步模型
- 怎樣理解阻塞非阻塞與同步非同步的區別?非同步
- ♻️同步和非同步;並行和併發;阻塞和非阻塞非同步並行
- ES6非同步方式全面解析非同步
- 併發-0-同步/非同步/阻塞/非阻塞/程式/執行緒非同步執行緒
- 聊聊執行緒與程式 & 阻塞與非阻塞 & 同步與非同步執行緒非同步
- 對於同步、非同步、阻塞、非阻塞的幾點淺薄理解非同步
- 程式執行緒、同步非同步、阻塞非阻塞、併發並行執行緒非同步並行
- 從同步原語看非阻塞同步以及Java中的應用Java
- Linux 資料同步方案Linux
- Java 非阻塞 IO 和非同步 IOJava非同步
- 【OS】同步非同步/阻塞非阻塞、併發並行序列的區分非同步並行
- 【死磕NIO】— 阻塞、非阻塞、同步、非同步,傻傻分不清楚非同步
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- ES6 Promise非同步程式設計Promise非同步程式設計
- 【深入淺出ES6】非同步Promise非同步Promise
- 非同步/同步,阻塞/非阻塞,單執行緒/多執行緒概念梳理非同步執行緒
- javascript非同步解決方案JavaScript非同步
- 非同步解決方案---promise非同步Promise
- VSCode官方的配置同步方案VSCode