首頁  >  文章  >  web前端  >  ES6之async+await 同步/非同步方案

ES6之async+await 同步/非同步方案

小云云
小云云原創
2018-02-05 14:17:481645瀏覽

這篇文章主要介紹了詳解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 确实是一个很优雅的方案。

相关推荐:

让Express支持async方法分享

NodeJs通过async和await处理异步的方法

Node.js中如何使用async函数

以上是ES6之async+await 同步/非同步方案的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn