這篇文章主要介紹了詳解ES6之async+await 同步/異步方案,本文以最簡明的方式來疏通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 确实是一个很优雅的方案。
以上是ES6之async+await同步/非同步方案詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!