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

以前寫了篇文《這一次,完全理解Promise原理》,剖析了Promise的相關原理,反應不錯,此次把學習到的相關的知識也寫下。前端

咱們都知道,Promise解決了回調地獄的問題,可是若是遇到複雜的業務,代碼裏面會包含大量的 then 函數,使得代碼依然不是太容易閱讀。es6

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

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

Generator函數簡介

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

有這樣一段代碼:微信

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

var hw = helloWorldGenerator();
複製代碼

調用及運行結果:異步

hw.next()// { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next()// { value: undefined, done: true }
複製代碼

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

Generator函數暫停恢復執行原理

要搞懂函數爲什麼能暫停和恢復,那你首先要了解協程的概念。異步編程

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

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

  1. 協程A開始執行
  2. 協程A執行到某個階段,進入暫停,執行權轉移到協程B
  3. 協程B執行完成或暫停,將執行權交還A
  4. 協程A恢復執行

協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續日後執行。它的最大優勢,就是代碼的寫法很是像同步操做,若是去除yield命令,簡直如出一轍。

執行器

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

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

  1. 回調函數。將異步操做包裝成 Thunk 函數,在回調函數裏面交回執行權。
  2. 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') //返回promise對象
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://xxx') //返回promise對象
    console.log('response2')
    console.log(response2)
}
run(foo);
複製代碼

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

async/await

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 函數的改進,體如今如下四點:

  1. 內置執行器。Generator 函數的執行必須依靠執行器,而 async 函數自帶執行器,無需手動執行 next() 方法。
  2. 更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
  3. 更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成當即 resolved 的 Promise 對象)。
  4. 返回值是 Promise。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,能夠直接使用 then() 方法進行調用。

這裏的重點是自帶了執行器,至關於把咱們要額外作的(寫執行器/依賴co模塊)都封裝了在內部。好比:

async function fn(args) {
  // ...
}
複製代碼

等同於:

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

function spawn(genF) { //spawn函數就是自動執行器,跟簡單版的思路是同樣的,多了Promise和容錯處理
  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/await執行順序

經過上面的分析,咱們知道async隱式返回 Promise 做爲結果的函數,那麼能夠簡單理解爲,await後面的函數執行完畢時,await會產生一個微任務(Promise.then是微任務)。可是咱們要注意這個微任務產生的時機,它是執行完await以後,直接跳出async函數,執行其餘代碼(此處就是協程的運做,A暫停執行,控制權交給B)。其餘代碼執行完畢後,再回到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 => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
複製代碼

分析這段代碼:

  • 執行代碼,輸出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

  • 最後,執行下一個宏任務,即執行setTimeout,輸出setTimeout

參考資料

Promise資料

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個有專業的技術人...

GitHub
相關文章
相關標籤/搜索