這篇手寫async函數及過程分析

前言

你盼世界,我盼望你無bug。Hello 你們好!我是霖呆呆!javascript

其實本文怎麼說呢,算不上是呆呆的純原創吧,由於呆呆也是參考晨曦老哥的手寫async await的最簡實現(20行)來寫的,包括案例啥的也是同樣,哈哈不過你們請放心我也是通過原做者受權的,並且參考的這篇文章,對手寫async函數說的也很清楚了。不過呆呆主要是在其中加上了一些本身的理解以及更加詳細的轉換過程,也算是本身的一個學習筆記吧。前端

因此若是您在看完呆呆寫的這篇文章後,還但願能夠再看一遍晨曦哥的原創,這樣對您的幫助應該會更大。java

(另外若是您以爲呆呆寫的還不錯的話還但願能夠給本篇和原創都點一個贊,畢竟呆呆也是借鑑的晨曦哥的,心裏有愧...啊啊啊...爲何感受我是真的臭不要臉,哪有求人家贊還帶送一個讚的😂)面試

正文

ES8推出的async/await容許咱們用更加優雅的方式來實現異步代碼,從某種角度上來講,它是Generator函數的語法糖,就像咱們常常說class是構造函數的語法糖同樣。bash

async案例

基本用法啥的我就不說了,這裏直接上一個簡單的案例,後面一步步的轉換過程都是以此案例做爲基礎。app

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';
}
test().then(res => console.log(res));
複製代碼

針對於上面的這段代碼,相信你們都沒有什麼疑問,很快咱們就能說出答案:異步

// 1s 後打印出
'data ' 'data'
// 1s 後打印出
'data2: ' 'data'
'success'
複製代碼

Generator案例

讓咱們先來回顧一下Generator最基本的一些概念:async

  • function後面加上*表示這是一個Generator函數,如function* testG(){}
  • 函數內部可使用yield來中斷函數的執行,即當每次執行到yield語句的時候,函數暫停執行
  • 暫停執行以後,須要調用next()纔會繼續執行Generator函數,直到碰到函數內下一個yield又會暫停
  • 以此循環,直到函數內有return或者函數內代碼所有執行完

其中還有很重要的一個知識點,就是每次調用next()的返回值,它是一個對象,這個對象會有兩個屬性:函數

  • valueyield語句後的表達式的結果
  • done:當前的Generator對象的邏輯塊是否執行完成

若是說咱們把上面的案例轉換爲Generator來實現的話,咱們想的可能會是這樣來寫:post

案例一:

const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))

function* testG () {
  const data = yield getData();
  console.log('data: ', data);
  const data2 = yield getData();
  console.log('data2: ', data2);
  return 'success';
}
var gen = testG();
// 以後手動調用3次
gen.next();
gen.next();
gen.next();
複製代碼

但是上面👆的代碼真的會和async案例中的執行結果同樣嗎?

當我打開控制檯的時候,結果卻出乎個人意料:

data: undefined
data2: undefined
複製代碼

What...?

你的suceess沒有打印就算了,你的datadata2居然都仍是undefined?這就有點難以理解了。

難道說是個人調用姿式不對嗎...

本着良好的職業素養,我對Generator研究了一波,而後修改了一下上面的代碼:

案例二:

const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))

function* testG () {
  const data = yield getData();
  console.log('data: ', data);
  const data2 = yield getData();
  console.log('data2: ', data2);
  return 'success';
}
var gen = testG();
// 手動調用3次且把每次的返回值打印出來看看
var dataPromise= gen.next();
console.log(dataPromise);
var dataPromise2 = gen.next('這個參數纔會被賦給data變量');
console.log(dataPromise2);
var dataPromise3 = gen.next('這個參數纔會被賦給data2變量');
console.log(dataPromise3);
複製代碼

能夠看到不一樣之處在於我把調用三次的返回值用了一個變量來盛放,而且在後面兩次調用gen.next()的時候傳遞了參數進去。

如今的輸出結果爲:

{value: Promise, done: false}
'data:', '這個參數纔會被賦給data變量'
{value: Promise, done: false}
'data2:', '這個參數纔會被賦給data2變量'
{value: Promise, done: false}
複製代碼

What X 2...?

哈哈哈,是否是有點摸不着頭腦了,這datadata2的返回值難道不是yield getData()的結果嗎?

我這裏的代碼明明就是const data = yield getData()呀,但是怎麼會變成了'這個參數纔會被賦給data變量'呢?

原來有些的你覺得並非真的你覺得,呆呆這裏詳細把每一步都分析一下:

OK👌,相信聰明的你如今必定弄懂Generator的執行機制了,它和咱們的async函數是有一些區別的。

async函數中,const data = await getData(),這個datagetData()resolve()的結果。

generator函數中,const data = yield getData(),其實只執行了yield getData()函數而已而且會把這個值做爲第一次調用gen.next()的返回值,也就是被dataPromise所接收。

data須要等到下一次調用gen.next()時纔會被賦值,這也就是爲何咱們在案例一中data會是undefined,由於那時候咱們調用gen.next()是沒有傳遞任何參數的。

Generator案例轉換async案例

顯然,上面👆兩個案例的執行結果和async案例中的結果是不同的,我只是將其作了一些拆分以便讓你更好的理解接下來我要作的事情。

嘻嘻😁,那麼如何讓案例二能打印出和async案例同樣的結果呢?

細心的小夥伴可能已經有了一些想法,dataPromise中會有調用返回的Promise對象,那麼咱們也就能拿到這個Promise的返回值'data'了,只須要使用.then()來進行一個鏈式調用,就像下面這樣:

const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))

function* testG () {
  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.value.then((value1) => {
  var dataPromise2 = gen.next(value1);
  dataPromise2.value.then((value2) => {
    var dataPromise3 = gen.next(value2)
    console.log(dataPromise3.value)
  })
})
複製代碼

如今的結果就是和async案例的結果同樣咯:

(若是你對這道題的結果仍是比較模糊的話請再仔細看一下我在案例二的那一大坨代碼註釋哦)

// 1s 後打印出
'data ' 'data'
// 1s 後打印出
'data2: ' 'data'
'success'
複製代碼

(注意⚠️,在每次調用gen.next()的時候,它的返回值是一個{ value: {}, done: false }這樣的對象,因此咱們想要使用返回的Promise的時候,須要用dataPromise.value來獲取)

throw()

在正式講解以前,讓咱們再來認識一下Generator的另外一個實例方法throw(),之因此說到它,是由於咱們最終的代碼須要用到它。

它和next()同樣,都是屬於Generator.prototype上的方法,且返回值也是和next()同樣。

讓咱們來看一個簡單的案例瞭解一下它是怎樣使用的哈:

(利用while(true){}循環,咱們建立了一個能夠無限調用的Generator函數)

function* gen () {
  while (true) {
    try {
      yield 'LinDaiDai'
    } catch (e) {
      console.log(e)
    }
  }
}
var g = gen();
g.next(); // { value: 'LinDaiDai', done: false }
g.next(); // { value: 'LinDaiDai', done: false }
g.throw(new Error('錯誤')); // Error: '錯誤'
複製代碼

並且你會發現,並非throw中必定要傳一個new Error()纔會被裏面的catch捕獲,你就算是傳遞一個別的類型的值進去,也會,例如我直接傳遞字符串'錯誤':

g.throw('錯誤'); // '錯誤'
複製代碼

它也會被捕獲。

Generator實現async?

咱們在搞懂了Generator的執行機制以後,就能夠來看看,async是怎樣用Generator來實現的了。

首先,讓咱們肯定一下咱們要作的事情:

const getData = () => new Promise(resolve => setTimeout(() => { resolve('data') }, 1000))

function* testG () { // 這個就是上面的那個案例
  const data = yield getData();
  console.log('data: ', data);
  const data2 = yield getData();
  console.log('data2: ', data2);
  return 'success';
}

// 咱們須要設計一個轉換函數
function asyncToGenerator (genFunc) {}

var test = asyncToGenerator(testG);
test().then(res => console.log(res));
複製代碼

那麼能夠看到,如今的關鍵就是在於實現一個asyncToGenerator轉換函數,它有如下特色:

  • 接收一個Generator函數
  • 返回一個Promise

Generator大家也看到了,它是很懶的,須要咱們每調用一次next()函數它才走一步,因此如何讓它自動的執行成爲了咱們須要思考的點。

呆呆這裏也再也不小氣了,直接上晨曦老哥的代碼再進行講解吧:

function asyncToGenerator (genFunc) {
  return function () {
    const gen = genFunc.apply(this, arguments);
    return new Promise((resolve, reject) => {
      function step (key, arg) {
        let generatorResult;
        try {
          generatorResult = gen[key](arg);
        } catch (err) {
          return reject(err);
        }
        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")
    })
  }
}
var gen = asyncToGenerator(testG)
gen().then(res => console.log(res))
複製代碼

怎麼樣?是否是以爲晨曦老哥很短啊,呸,寫的很精簡呀 😄。

若是以爲有點吃力的話不要緊,呆呆會將每一步仔細拆分着說。

1. 總體結構

讓咱們來設想一下asyncToGenerator的大概樣子了,它也許是長這樣的:

function asyncToGenerator (genFunc) {
  return function () {
    return new Promise((resolve, reject) => {})
  }
}
複製代碼

依照要求,接收一個Generator函數,返回一個Promise,上面這種結構是徹底知足的。但是若是是想要返回Promise的話,爲何還要把它包到一個函數裏面呢,不該該是這樣寫嗎:

function asyncToGenerator (genFunc) {
  return new Promise((resolve, reject) => {})
}
複製代碼

唔...這樣寫當然也能夠,不過別忘了咱們的調用方式:

var test = asyncToGenerator(testG);
test().then(res => console.log(res));
複製代碼

咱們通常是會把asyncToGenerator的返回值用一個變量來盛放的,因此若是你不包到函數裏的話,就只能這樣調用了:

asyncToGenerator(testG).then(res => console.log(res))
複製代碼

這顯然不是咱們想要的。

2. 如何保證自動調用

有了asyncToGenerator函數的總體結構以後咱們就要開始考慮如何讓它自動調用這一個個的next()呢?

咦~你是否是想到了什麼?

遞歸?

嘻嘻😁,確實像這種須要循環調用的時候確實容易讓人想到遞歸,這裏其實也是能夠的。

因此如今咱們就得先肯定遞歸的終止條件是什麼。

Generator函數中,什麼狀況纔算是該對象的邏輯代碼執行完了呢?這個其實前面也已經提到了,當返回的done屬性爲true時就能夠肯定已經執行完了,遞歸也該結束了。

知道了終止的條件以後,咱們就須要把整個遞歸結束,結合上面👆咱們已經設計好的總體結構,這其實很簡單,直接return一個resolve()或者reject()就能夠作到:

function asyncToGenerator (genFunc) {
	return function () {
		return new Promise((resolve, reject) => {
      // 終止條件知足時,直接調用reject()來退出, value爲最終的值(後面會說到)
      return resolve(value);
      // 或者 return reject(error);
    })
	}
}
複製代碼

OK👌,終止條件和怎麼終止都已經知道了,讓咱們接着往下看。

在這裏,咱們能夠寫一個step函數,而後配合Promise.resolve()來實現循環調用,相似於這樣:

function asyncToGenerator (genFunc) {
  return function () {
    return new Promise((resolve, reject) => {
      function step () {
        return Promise.resolve(value).then(val => {
          step()
        })
      }
      step();
    })
  }
}
複製代碼

因此這時候咱們就得看看Promise.resolve(value)中的value是哪來的了,它其實是咱們在每次調用gen.next()返回值中的value,對應着案例二的代碼:

yield getData()

// 也就是 getData()的返回值
// 也就是 Promise{<resolve>, 'data'}
複製代碼

因此此時咱們的代碼就變成了這樣:

function asyncToGenerator (genFunc) {
    return function () {
+ const gen = genFunc.apply(this, arguments);
      return new Promise((resolve, reject) => {
      function step () {
+ let generatorResult = gen.next();
+ const { value, done } = generatorResult;
+ if (done) {
+ return resolve(value);
+ } else {
          return Promise.resolve(value).then(val => {
             step()
           })
+ }
       }
      step();
    })
  }
}
複製代碼

這段代碼我加上了兩個功能:

  1. const gen = getFunc.apply(this, arguments)來實現相似const gen = testG()這樣的代碼,是爲了先調用generator函數來生成迭代器。
  2. setp中調用了gen.next(),並保存結果到generatorResult上,同時判斷終止條件done

3. 傳值以及異常處理

OK👌,完成了上述步驟後,顯然仍是不夠的,咱們至少還有兩點沒有考慮到:

  • 沒有把調用gen.next()的結果傳遞給後面的gen.next()
  • 沒有對異常狀況做處理

首先,前面也提到了,next()throw()調用時的返回值都是這樣的格式:

{ value: {}, done: false }
複製代碼

那咱們是否是就能夠把這兩個方法名當成一個參數傳遞到下一個step中呢?

而每次Promise.resolve(value)這裏的結果咱們也能夠把它當成參數傳遞到下一個step中。

因此如今再讓咱們來看看最終的代碼:

function asyncToGenerator (genFunc) {
  return function () {
    const gen = genFunc.apply(this, arguments);
    return new Promise((resolve, reject) => {
      function step (key, arg) {
        let generatorResult;
        try {
          generatorResult = gen[key](arg);
        } catch (err) {
          return reject(err);
        }
        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")
    })
  }
}
var gen = asyncToGenerator(testG)
gen().then(res => console.log(res))
複製代碼

新加的代碼主要作了這麼幾件事:

  1. step函數增長兩個字段:key爲方法名(next或者throw);arg爲上一次gen[key]()
  2. 使用try/catch來捕獲執行gen[key](arg)時的異常
  3. .then()中增長第二個參數來捕獲異常

這裏須要注意的一點是,Promise.resolve(value)中的value它是一個Promise,爲何呢?

讓咱們來看看generatorResult

{ value: Promise{<resolve>, 'data'}, done: false }
複製代碼

對應着案例二,其實也就是yield getData()這段語句中getData()返回的那個Promise

而下一個step("next")必須等value這個Promiseresolve的時候纔會被調用。

參考文章

後語

你盼世界,我盼望你無bug。這篇文章就介紹到這裏。

OK👌,狀況就是這麼一個狀況,在咱們弄懂這一步一步的原理以後再來記就不難了 😄。

喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai 或者掃一掃下面的二維碼👇👇👇.

我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉

你的鼓勵就是我持續創做的主要動力 😊.

相關推薦:

《全網最詳bpmn.js教材》

《【建議改爲】讀完這篇你還不懂Babel我給你寄口罩》

《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》

《【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理)》

《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》

《【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)》

《【精】從206個console.log()徹底弄懂數據類型轉換的前世此生(上)》

《霖呆呆的近期面試128題彙總(含超詳細答案) | 掘金技術徵文》

相關文章
相關標籤/搜索