手寫async await的最簡實現(20行)

  • 2020 年 4 月 11 日
  • 筆記

前言

如果讓你手寫async函數的實現,你是不是會覺得很複雜?這篇文章帶你用20行搞定它的核心。

經常有人說async函數是generator函數的語法糖,那麼到底是怎麼樣一個糖呢?讓我們來一層層的剝開它的糖衣。

有的同學想說,既然用了generator函數何必還要實現async呢?

這篇文章的目的就是帶大家理解清楚async和generator之間到底是如何相互協作,管理非同步的。

示例

const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000))    async function test() {    const data = await getData()    console.log('data: ', data);    const data2 = await getData()    console.log('data2: ', data2);    return 'success'  }    // 這樣的一個函數 應該再1秒後列印data 再過一秒列印data2 最後列印success  test().then(res => console.log(res))  複製程式碼

思路

對於這個簡單的案例來說,如果我們把它用generator函數表達,會是怎麼樣的呢?

function* testG() {    // await被編譯成了yield    const data = yield getData()    console.log('data: ', data);    const data2 = yield getData()    console.log('data2: ', data2);    return 'success'  }  複製程式碼

我們知道,generator函數是不會自動執行的,每一次調用它的next方法,會停留在下一個yield的位置。

利用這個特性,我們只要編寫一個自動執行的函數,就可以讓這個generator函數完全實現async函數的功能。

const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000))    var test = asyncToGenerator(      function* testG() {        // await被編譯成了yield        const data = yield getData()        console.log('data: ', data);        const data2 = yield getData()        console.log('data2: ', data2);        return 'success'      }  )    test().then(res => console.log(res))  複製程式碼

那麼大體上的思路已經確定了,

asyncToGenerator接受一個generator函數,返回一個promise

關鍵就在於,裡面用yield來劃分的非同步流程,應該如何自動執行。

如果是手動執行

在編寫這個函數之前,我們先模擬手動去調用這個generator函數去一步步的把流程走完,有助於後面的思考。

function* testG() {    // await被編譯成了yield    const data = yield getData()    console.log('data: ', data);    const data2 = yield getData()    console.log('data2: ', data2);    return 'success'  }  複製程式碼

我們先調用testG生成一個迭代器

// 返回了一個迭代器  var gen = testG()  複製程式碼

然後開始執行第一次next

// 第一次調用next 停留在第一個yield的位置  // 返回的promise里 包含了data需要的數據  var dataPromise = gen.next()  複製程式碼

這裡返回了一個promise,就是第一次getData()所返回的promise,注意

const data = yield getData()  複製程式碼

這段程式碼要切割成左右兩部分來看,第一次調用next,其實只是停留在了yield getData()這裡,

data的值並沒有被確定。

那麼什麼時候data的值會被確定呢?

下一次調用next的時候,傳的參數會被作為上一個yield前面接受的值

也就是說,我們再次調用gen.next('這個參數才會被賦給data變數')的時候

data的值才會被確定為'這個參數才會被賦給data變數'

gen.next('這個參數才會被賦給data變數')    // 然後這裡的data才有值  const data = yield getData()    // 然後列印出data  console.log('data: ', data);    // 然後繼續走到下一個yield  const data2 = yield getData()  複製程式碼

然後往下執行,直到遇到下一個yield,繼續這樣的流程…

這是generator函數設計的一個比較難理解的點,但是為了實現我們的目標,還是得去學習它~

藉助這個特性,如果我們這樣去控制yield的流程,是不是就能實現非同步串列了?

function* testG() {    // await被編譯成了yield    const data = yield getData()    console.log('data: ', data);    const data2 = yield getData()    console.log('data2: ', data2);    return 'success'  }    var gen = testG()    var dataPromise = gen.next()    dataPromise.then((value1) => {      // data1的value被拿到了 繼續調用next並且傳遞給data      var data2Promise = gen.next(value1)        // console.log('data: ', data);      // 此時就會列印出data        data2Promise.then((value2) => {          // data2的value拿到了 繼續調用next並且傳遞value2           gen.next(value2)            // console.log('data2: ', data2);          // 此時就會列印出data2      })  })  複製程式碼

這樣的一個看著像callback hell的調用,就可以讓我們的generator函數把非同步安排的明明白白。

實現

有了這樣的思路,實現這個高階函數就變得很簡單了。

先整體看一下結構,有個印象,然後我們逐行注釋講解。

function asyncToGenerator(generatorFunc) {      return function() {        const gen = generatorFunc.apply(this, arguments)        return new Promise((resolve, reject) => {          function step(key, arg) {            let generatorResult            try {              generatorResult = gen[key](arg)            } catch (error) {              return reject(error)            }            const { value, done } = generatorResult            if (done) {              return resolve(value)            } else {              return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))            }          }          step("next")        })      }  }  複製程式碼

不多不少,22行。

接下來逐行講解。

function asyncToGenerator(generatorFunc) {    // 返回的是一個新的函數    return function() {        // 先調用generator函數 生成迭代器      // 對應 var gen = testG()      const gen = generatorFunc.apply(this, arguments)        // 返回一個promise 因為外部是用.then的方式 或者await的方式去使用這個函數的返回值的      // var test = asyncToGenerator(testG)      // test().then(res => console.log(res))      return new Promise((resolve, reject) => {          // 內部定義一個step函數 用來一步一步的跨過yield的阻礙        // key有next和throw兩種取值,分別對應了gen的next和throw方法        // arg參數則是用來把promise resolve出來的值交給下一個yield        function step(key, arg) {          let generatorResult            // 這個方法需要包裹在try catch中          // 如果報錯了 就把promise給reject掉 外部通過.catch可以獲取到錯誤          try {            generatorResult = gen[key](arg)          } catch (error) {            return reject(error)          }            // gen.next() 得到的結果是一個 { value, done } 的結構          const { value, done } = generatorResult            if (done) {            // 如果已經完成了 就直接resolve這個promise            // 這個done是在最後一次調用next後才會為true            // 以本文的例子來說 此時的結果是 { done: true, value: 'success' }            // 這個value也就是generator函數最後的返回值            return resolve(value)          } else {            // 除了最後結束的時候外,每次調用gen.next()            // 其實是返回 { value: Promise, done: false } 的結構,            // 這裡要注意的是Promise.resolve可以接受一個promise為參數            // 並且這個promise參數被resolve的時候,這個then才會被調用            return Promise.resolve(              // 這個value對應的是yield後面的promise              value            ).then(              // value這個promise被resove的時候,就會執行next              // 並且只要done不是true的時候 就會遞歸的往下解開promise              // 對應gen.next().value.then(value => {              //    gen.next(value).value.then(value2 => {              //       gen.next()              //              //      // 此時done為true了 整個promise被resolve了              //      // 最外部的test().then(res => console.log(res))的then就開始執行了              //    })              // })              function onResolve(val) {                step("next", val)              },              // 如果promise被reject了 就再次進入step函數              // 不同的是,這次的try catch中調用的是gen.throw(err)              // 那麼自然就被catch到 然後把promise給reject掉啦              function onReject(err) {                step("throw", err)              },            )          }        }        step("next")      })    }  }  複製程式碼

源碼地址

這個 js文件 的程式碼可以直接放進瀏覽器里運行,歡迎調戲。

總結

本文用最簡單的方式實現了asyncToGenerator這個函數,這是babel編譯async函數的核心,當然在babel中,generator函數也被編譯成了一個很原始的形式,本文我們直接以generator替代。

這也是實現promise串列的一個很棒的模式,如果本篇文章對你有幫助,點個贊就好啦。

❤️感謝大家