天天看點

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

前言

如果讓你手寫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串行的一個很棒的模式,如果本篇文章對你有幫助,點個贊就好啦。

❤️感謝大家