async/await 原理及執行順序分析

2021-02-19 前端Q

之前寫了篇文章《這一次,徹底理解Promise原理》,剖析了Promise的相關原理。我們都知道,Promise解決了回調地獄的問題,但是如果遇到複雜的業務,代碼裡面會包含大量的 then 函數,使得代碼依然不是太容易閱讀。

基於這個原因,ES7 引入了 async/await,這是 JavaScript 異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力,並且使得代碼邏輯更加清晰,而且還支持 try-catch 來捕獲異常,非常符合人的線性思維。

所以,要研究一下如何實現 async/await。總的來說,async 是Generator函數的語法糖,並對Generator函數進行了改進。

Generator 函數是一個狀態機,封裝了多個內部狀態。執行 Generator 函數會返回一個遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態,但是只有調用next方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函數。yield表達式就是暫停標誌。

有這樣一段代碼:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

調用及運行結果:

hw.next()
hw.next()
hw.next()
hw.next()

由結果可以看出,Generator函數被調用時並不會執行,只有當調用next方法、內部指針指向該語句時才會執行,即函數可以暫停,也可以恢復執行。每次調用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。

要搞懂函數為何能暫停和恢復,那你首先要了解協程的概念。

一個線程(或函數)執行到一半,可以暫停執行,將執行權交給另一個線程(或函數),等到稍後收回執行權的時候,再恢復執行。這種可以並行執行、交換執行權的線程(或函數),就稱為協程。

協程是一種比線程更加輕量級的存在。普通線程是搶先式的,會爭奪cpu資源,而協程是合作的,可以把協程看成是跑在線程上的任務,一個線程上可以存在多個協程,但是在線程上同時只能執行一個協程。它的運行流程大致如下:

協程A開始執行

協程A執行到某個階段,進入暫停,執行權轉移到協程B

協程B執行完成或暫停,將執行權交還A

協程A恢復執行

協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是代碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。

通常,我們把執行生成器的代碼封裝成一個函數,並把這個執行生成器代碼的函數稱為執行器,co 模塊就是一個著名的執行器。

Generator 是一個異步操作的容器。它的自動執行需要一種機制,當異步操作有了結果,能夠自動交回執行權。兩種方法可以做到這一點:

回調函數。將異步操作包裝成 Thunk 函數,在回調函數裡面交回執行權。

Promise 對象。將異步操作包裝成 Promise 對象,用then方法交回執行權。

一個基於 Promise 對象的簡單自動執行器:

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

我們使用時,可以這樣使用即可,

function* foo() {
    let response1 = yield fetch('https://xxx') 
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://xxx') 
    console.log('response2')
    console.log(response2)
}
run(foo);

上面代碼中,只要 Generator 函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。通過使用生成器配合執行器,就能實現使用同步的方式寫出異步代碼了,這樣也大大加強了代碼的可讀性。

ES7 中引入了 async/await,這種方式能夠徹底告別執行器和生成器,實現更加直觀簡潔的代碼。根據 MDN 定義,async 是一個通過異步執行並隱式返回 Promise 作為結果的函數。可以說async 是Generator函數的語法糖,並對Generator函數進行了改進。

前文中的代碼,用async實現是這樣:

const foo = async () => {
    let response1 = await fetch('https://xxx') 
    console.log('response1')
    console.log(response1)
    let response2 = await fetch('https://xxx') 
    console.log('response2')
    console.log(response2)
}

一比較就會發現,async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。

async函數對 Generator 函數的改進,體現在以下四點:

內置執行器。Generator 函數的執行必須依靠執行器,而 async 函數自帶執行器,無需手動執行 next() 方法。

更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裡有異步操作,await表示緊跟在後面的表達式需要等待結果。

更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)。

返回值是 Promise。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then() 方法進行調用。

這裡的重點是自帶了執行器,相當於把我們要額外做的(寫執行器/依賴co模塊)都封裝了在內部。比如:

async function fn(args) {
  
}

等同於:

function fn(args) {
  return spawn(function* () {
    
  });
}

function spawn(genF) { 
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

通過上面的分析,我們知道async隱式返回 Promise 作為結果的函數,那麼可以簡單理解為,await後面的函數執行完畢時,await會產生一個微任務(Promise.then是微任務)。但是我們要注意這個微任務產生的時機,它是執行完await之後,直接跳出async函數,執行其他代碼。其他代碼執行完畢後,再回到async函數去執行剩下的代碼,然後把await後面的代碼註冊到微任務隊列當中。我們來看個例子:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')
}, 0)

new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})

console.log('script end')

分析這段代碼:

執行代碼,輸出script start。

執行async1(),會調用async2(),然後輸出async2 end,此時將會保留async1函數的上下文,然後跳出async1函數。

遇到setTimeout,產生一個宏任務

執行Promise,輸出Promise。遇到then,產生第一個微任務

繼續執行代碼,輸出script end

代碼邏輯執行完畢(當前宏任務執行完畢),開始執行當前宏任務產生的微任務隊列,輸出promise1,該微任務遇到then,產生一個新的微任務

執行產生的微任務,輸出promise2,當前微任務隊列執行完畢。執行權回到async1

執行await,實際上會產生一個promise返回,即

let promise_ = new Promise((resolve,reject){ resolve(undefined)})

執行完成,執行await後面的語句,輸出async1 end

相關焦點

  • 8 張圖幫你一步步看清 async/await 和 promise 的執行順序
    說實話,關於js的異步執行順序,宏任務、微任務這些,或者async/await這些慨念已經有非常多的文章寫了。但是怎麼說呢,簡單來說,業務中很少用async,不太懂async呢,研究了一天,感覺懂了,所手癢想寫一篇 ,哈哈畢竟自己學會的知識,如果連表達清楚都做不到,怎麼能指望自己用好它呢?
  • Async/Await有什麼用?
    .*/)本文中的這類注釋可以幫你理解代碼的執行順序。永遠返回 Promise現在我們知道異步函數不是什麼,但是它們是什麼呢?異步函數的第一個超級功能:總是返回一個 promise。當你使用 await 語句時,javascript 會暫停 async 函數的執行,等待 promise 返回一個值,然後繼續執行。
  • 理解JavaScript 的 async/await
    1. async 和 await 在幹什麼任意一個名稱都是有意義的,先從字面意思來理解。async 是「異步」的簡寫,而 await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。
  • async/await,了解一下?
    相較於 Generator,Async 函數的改進在於下面幾點:Generator 函數的執行必須依靠執行器,而 Async() 函數自帶執行器,調用方式跟普通函數的調用一樣。 Async 和 await 相較於 * 和 yield 更加語義化。 async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then()方法進行調用。
  • 深入async/await知多少
    工作原理      async/await簡單來說只是一個語法糧,它只是告訴編譯器要把這些代碼編譯成一個異步狀態機。      async/await是一個異步處理模型,但並不能說明所有的async/await都是異步處理;具體要看Awaiter狀態機是由誰觸發的,當上層方法邏輯是同步或IO同步完成的情況那await後面的代碼則由同當前線程觸發執行,如果上層方法是異步完成的情況下則由對應相關異步完成的線程調用;所以async/await也有些情況是同步完成的,只是這種情況在
  • [完結篇] - 理解異步之美 --- promise與async await(三)
    因為在用法上promise要比async await難一些,而且promise本身又不是一個語法糖。沒有掌握的時候用起來就會有很多顧慮,async await卻沒有這種顧慮,用法簡單、語義清晰。用await關鍵字的時候就是在告訴下面的代碼,這塊你得給我等著,wait我執行完了才能輪到你 understand?總之await吊極了。await在什麼時候可以用? 只有在async函數體內部使用,而且這個作用範圍是不可以繼承下去的。
  • JavaScript中的async/await的用法和理解
    今天就說一說「JavaScript中的async/await的用法和理解」程式語言中任意一個關鍵字都是有意義的,我們先從字面意思來理解。await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用於申明一個 function 是異步的,而 await 用於等待一個異步方法執行完成。
  • 你必須了解的JavaScript關鍵字async和await
    renderer: new Term() })) .then(txt => console.log(txt)) .catch(reason => console.error(reason));}執行順序就和你閱讀代碼的順序一致
  • Python async/await教程
    async/await更新的和更清潔的語法是使用async/await關鍵字,async在Python 3.5中引入,用於作為一個協同程序聲明一個函數,就像@asyncio.coroutine裝飾器所做的,通過把它放到函數定義前使它應用於函數:
  • 如何在 JS 循環中正確使用 async 與 await
    = await getNumFruit(fruit);    console.log(numFruit);  }  console.log('End')}當使用await時,希望JavaScript暫停執行,直到等待 promise 返回處理結果。
  • 理解C#中的 async await
    await 內部的奧秘。3|0多個 async await 嵌套理解了async await的簡單使用,那你可曾想過,如果有多個 async await 嵌套,那會出現什麼情況呢?原來,async await 的嵌套也就是狀態機的嵌套,相信你通過上面的狀態機狀態流轉,也能夠梳理除真正的執行邏輯,那我們就只看一下線程狀態吧:
  • C# 中的Async 和 Await 的用法詳解
    作者:依樂祝原文連結:https://www.cnblogs.com/yilezhu/p/10555849.html寫在前面自從C# 5.0時代引入async和await關鍵字後,異步編程就變得流行起來。尤其在現在的.NET Core時代,如果你的代碼中沒有出現async或者await關鍵字,都會讓人感覺到很奇怪。
  • javascript解決異步async、await和co庫的實現
    相信大家都聽說過js中的回調地獄給代碼維護帶來了很大的阻礙,應用而生的也給出了N解決方案,從最初的promise,到co庫,再到es規範提供的api async、await等!接下來咱們聊的話題就是async和co庫的具體實現在學習前咱們了解幾個小知識點吧!
  • 實現一個 async/await (typescript 版)
    這次我們來實現一個 typescript 版本的 async/await。關於 async/await 的原理的文章,網上也有很多了,但是本文既然是使用 typescript 來寫,我們的 async/await 也是要能夠通過用戶傳入的函數自動推斷出結果,所以如何對其編寫 typescript 定義也是本文的一個重要板塊。
  • 如何正確合理使用 JavaScript async/await
    在本文中,將從不同的角度探討 async/await,並演示如何正確有效地使用這對兄弟。async 作用是什麼從 MDN 可以看出:async 函數返回的是一個 Promise 對象。async 函數調用不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 對象中異步執行。async/await 帶給我們的最重要的好處是同步編程風格。讓我們看一個例子:
  • Async:簡潔優雅的異步之道
    G函數通過在 function後使用 *來標識此為G函數,而A函數則是在 function前加上 async關鍵字。在G函數中可以使用 yield命令暫停執行和交出執行權,而A函數是使用 await來等待異步返回結果。很明顯, async和 await更為語義化。
  • async,await執行流看不懂?看完這篇以後再也不會了
    昨天有朋友在公眾號發消息說看不懂await,async執行流,其實看不懂太正常了,因為你沒經過社會的毒打,沒吃過牢飯就不知道自由有多重要,沒生過病就不知道健康有多重要,沒用過ContinueWith就不知道await,async有多重要,下面我舉兩個案例佐證一下?
  • 代碼詳解:Async/Await優於基礎Promises的7大原因
    函數前有關鍵詞async。關鍵詞await 只能用於async定義的函數之內。任一async函數都能隱式返回promise,其解析值是從該函數返回的任意值(在此指字符串「done」)。2. 上一點意味著不能在代碼頂部使用await,因為它不在async定義的函數範圍之中。
  • 壓箱底筆記:Promise和Async/await的理解和使用
    Promise的API說明4.1 API 說明4.2 Promise的幾個關鍵問題5. async與await1.async function 用來定義一個返回 AsyncFunction 對象的異步函數。異步函數是指通過事件循環異步執行的函數,它會通過一個隱式的 Promise 返回其結果,。如果你在代碼中使用了異步函數,就會發現它的語法和結構會更像是標準的同步函數。MDN async_functionawait  操作符用於等待一個Promise 對象。
  • 如何用實例掌握Async/Await
    今天讓我們一起來探討如何用實例掌握Async/Await目錄1、簡介(callbacks, promises, async/await)2、實例—貨幣轉換器從2個API’s接收異步數據。Async函數是通過在函數聲明之前加上Async來創建的,如下所示:異步函數可以用await暫停,await是只能在異步函數中使用的關鍵字。await返回異步函數完成後返回的任何內容。