Async是如何被 JavaScript 實現(xiàn)的
大廠技術(shù)??高級前端??Node進(jìn)階
點擊上方?程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
太久沒和大家見面了,因為最近業(yè)務(wù)上接了新的項目所以寫文的時間被嚴(yán)重擠壓。
這篇 Async 是如何被實現(xiàn)的,其實斷斷續(xù)續(xù)已經(jīng)在草稿箱里躺了很久了。終于在一個夜黑風(fēng)高的周六晚上可以給他畫上一個句號。
引言
無論是面試過程還是日常業(yè)務(wù)開發(fā),相信大多數(shù)前端開發(fā)者可以熟練使用 Async/Await 作為異步任務(wù)的終極處理方案。
但是對于 Async 函數(shù)的具體實現(xiàn)過程只是知其然不知所以然,僅僅了解它是基于 Promise 和 Generator 生成器函數(shù)的語法糖。
提及 JavaScript 中 Async 函數(shù)的內(nèi)部實現(xiàn)原理,大多數(shù)開發(fā)者并不清楚這一過程。甚至從來沒有思考過 Async 所謂語法糖是如何被 JavaScript 組合而來的。
別擔(dān)心,文中會帶你分析 Async 語法在低版本瀏覽器下的 polyfill 實現(xiàn),同時我也會手把手帶你基于 Promise 和 Generator 來實現(xiàn)所謂的 Async 語法。
我們會從以下方面來逐步攻克 Async 函數(shù)背后的實現(xiàn)原理:
?? Promise 章節(jié)梳理,從入門到源碼帶你掌握 Promise 應(yīng)用。
?? 什么是生成器函數(shù)?Generator 生成器函數(shù)基本特征梳理。
?? Generator 是如何被實現(xiàn)的,Babel 如何在低版本瀏覽器下實現(xiàn) Generator 生成器函數(shù)。
?? 作為通用異步解決方案的 Generator 生成器函數(shù)是如何解決異步方案。
?? 開源 Co 庫的基本原理實現(xiàn)。
?? Async/Await 函數(shù)為什么會被稱為語法糖,它究竟是如何被實現(xiàn)的。
相信讀完文章的你,對于 Async/Await 真正可以做到“知其然,知其所以然”。
Promise
所謂 Async/Await 語法我們提到本質(zhì)上它是基于Promise 和 Generator 生成器函數(shù)的語法糖。
關(guān)于 Promise 這篇文章中我就不過于展開他的基礎(chǔ)和原理部分了,網(wǎng)絡(luò)中對于介紹 Promise 相關(guān)的文章目前已經(jīng)非常優(yōu)秀了。如果有興趣深入 Promise 的同學(xué)可以查看:
?? JavaScript Promise MDN Docs:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
Promise 基礎(chǔ)使用準(zhǔn)則,MDN 上給出了詳盡的說明和實例,強(qiáng)烈建議對于 Promise 陌生的同學(xué)可以查閱 MDN 鞏固 Promise 基礎(chǔ)知識。
?? Promise A+ 規(guī)范:https://promisesaplus.com/
Promise A+ 實現(xiàn)準(zhǔn)則,不同瀏覽器/環(huán)境下對于 Promise 都有自己的實現(xiàn),它們都會依照同一規(guī)范標(biāo)準(zhǔn)去實現(xiàn) Promise 。
我在 ?? 這個地址:https://github.com/19Qingfeng/notes/blob/master/promise/core/index.js?按照規(guī)范實現(xiàn)過一版完整的 Promise ,有興趣的通許可以自行查閱代碼進(jìn)行 Promise 原理鞏固。
?? V8 Promise源碼全面解讀:https://juejin.cn/post/7055202073511460895
關(guān)于 Promise 中各種邊界應(yīng)用以及深層次 Promise 原理實現(xiàn),筆者強(qiáng)烈建議有興趣更深層次的同學(xué)結(jié)合月夕的這篇文章去參照閱讀。
生成器函數(shù)
關(guān)于 Generator 生成器函數(shù)與 Iterator 迭代器,大多數(shù)開發(fā)者在日常應(yīng)用中可能并不如 Promise 那么常見。
所以針對于 Generator 我會稍微和大家從基礎(chǔ)開始講起。
Generator 概念
Generator 基礎(chǔ)入門
所謂 Generator 函數(shù)它是協(xié)程在 ES6 的實現(xiàn),最大特點就是可以交出函數(shù)的執(zhí)行權(quán)(即擁有暫停函數(shù)執(zhí)行的效果)。
function*?gen()?{
??yield?1;
??yield?2;
??yield?3;
}
let?g?=?gen();
g.next();?//?{?value:?1,?done:?false?}
g.next();?//?{?value:?2,?done:?false?}
g.next();?//?{?value:?3,?done:?false?}
g.next();?//?{?value:?undefined,?done:?true?}
g.next();?//?{?value:?undefined,?done:?true?}
復(fù)制代碼
上述的函數(shù)就是一個 Generator 生成器函數(shù)的例子,我們通過在函數(shù)聲明后添加一個 * 的語法創(chuàng)建一個名為 gen 的生成器函數(shù)。
調(diào)用創(chuàng)建的生成器函數(shù)會返回一個 Generator { } 生成器實例對象。
初次接觸生成器函數(shù)的同學(xué),看到上面的例子可能稍微會有點懵。什么是 Generator 實例對象,函數(shù)中的 yield 關(guān)鍵字又是做什么的,我們應(yīng)該如何使用它呢?
別著急,接下來我們來一步一揭開這些迷惑。
所謂返回的 g 生成器對象你可以簡單的將它理解成為類似這樣的一個對象結(jié)構(gòu):
{
????next:?function?()?{
????????return?{
????????????done:Boolean,?//?done表示生成器函數(shù)是否執(zhí)行完畢?它是一個布爾值
????????????value:?VALUE,?//?value表示生成器函數(shù)本次調(diào)用返回的值
????????}
????}
}
復(fù)制代碼
首先,我們通過 let g = gen()調(diào)用生成器函數(shù)創(chuàng)建了一個生成器對象g,此時 g 擁有 next 上述結(jié)構(gòu)的 next 方法。
這一步,我們成為 g 為返回的生成器對象, gen 為生成器函數(shù)。通過調(diào)用生成器函數(shù) gen 返回了生成器對象 g 。
之后,生成器對象中的 next 方法每次調(diào)用會返回一次 { value:VALUE, done:boolean }的對象。
每次調(diào)用生成器對象的 next 方法會返回一個上述類型的 object:
其中 done 表示生成器函數(shù)是否執(zhí)行完畢,而 value 表示生成器函數(shù)中本次 yield 對應(yīng)的值。
部分沒有接觸過的同學(xué)可能不太了解這一過程,我們來詳細(xì)拆開上述函數(shù)的執(zhí)行過程來看看:
首先調(diào)用 gen() 生成器函數(shù)返回 g 生成器對象。
其次返回的 g 生成器對象中擁有一個 next 的方法。
每當(dāng)我們調(diào)用 g.next() 方法時,生成器函數(shù)緊跟著上一次進(jìn)行執(zhí)行,直到函數(shù)碰到 yield 關(guān)鍵值。
yield 關(guān)鍵字會停止函數(shù)執(zhí)行并將 yield 后的值返回作為本次調(diào)用 next 函數(shù)的 value 進(jìn)行返回。
同時,如果本次調(diào)用 g.next() 導(dǎo)致生成器函數(shù)執(zhí)行完畢,那么此時 done 會變成 true 表示該函數(shù)執(zhí)行完畢,反之則為 false 。
比如當(dāng)我們調(diào)用 let g = gen() 時,會返回一個生成器函數(shù),它擁有一個 next 方法。
之后當(dāng)?shù)谝淮握{(diào)用 g.next() 方法時,會執(zhí)行生成器函數(shù) gen 。函數(shù)會進(jìn)行執(zhí)行,直到碰到 yield 關(guān)鍵字會進(jìn)行暫停,此時函數(shù)會暫停到 yield 1 語句執(zhí)行完畢,將 1 賦給 value
同時因為生成器函數(shù) gen 并沒有執(zhí)行完畢,所以此時 done 應(yīng)該為 false 。所以此時首次調(diào)用 g.next() 函數(shù)返回的應(yīng)該是 { value: 1, done: false }。
之后,我們第二次調(diào)用 g.next() 方法時,函數(shù)會從上一次的中斷結(jié)果后進(jìn)行執(zhí)行。也就是會繼續(xù) yield 2 語句。
當(dāng)遇到 yield 2 時,又因為碰到了 yield 語句。此時函數(shù)又會被中斷,因為此時函數(shù)并沒有執(zhí)行完成,并且yield 語句后緊挨著的是 2 所以第二個 g.next() 會返回 { value: 2 , done: false }。
同樣,yield 3; 回和前兩次執(zhí)行邏輯相同。
需要額外注意的是,當(dāng)我們第四次調(diào)用迭代器 g.next() 時,因為第三次 g.next() 結(jié)束時生成器函數(shù)已經(jīng)執(zhí)行完畢了。所以再次調(diào)用 g.next() 時,由于函數(shù)結(jié)束 done 會變?yōu)?false 。同時因為函數(shù)不存在返回值,所以 value 為 undefined。
上邊是一個基于 Generator 函數(shù)的簡單執(zhí)行過程,其實它的本質(zhì)非常簡單:
調(diào)用生成器函數(shù)會返回一個生成器對象,每次調(diào)用生成器對象的 next 方法會執(zhí)行函數(shù)到下一次 yield 關(guān)鍵字停止執(zhí)行,并且返回一個 { value: Value, done: boolean }的對象。
上述執(zhí)行過程,我稍稍用了一些篇幅來描述這一簡單的過程。如果看到這里你還是沒有上面的 Demo 含義,那么此時請你停下往下的進(jìn)度,會到開頭一定要搞清楚這個簡單的 Demo 。
Generator 函數(shù)返回值
在掌握了基礎(chǔ) Generator 函數(shù)和 yield 關(guān)鍵字后,趁熱打鐵讓我們來一舉攻克 Generator 生成器函數(shù)的進(jìn)階語法。
老樣子,我們先來看這樣一段代碼:
function*?gen()?{
??const?a?=?yield?1;
??console.log(a,'this?is?a')
??const?b?=?yield?2;
??console.log(b,'this?is?b')
??const?c?=?yield?3;
??console.log(c,'this?is?c')
}
let?g?=?gen();
g.next();?//?{?value:?1,?done:?false?}
g.next('param-a');?//?{?value:?2,?done:?false?}
g.next('param-b');?//?{?value:?3,?done:?false?}
g.next('param-c');?//?{?value:?undefined,?done:?true?}
//?控制臺會打印:
//?param-a?this?is?a
//?param-b?this?is?b
//?param-c?this?is?c
復(fù)制代碼
這里,我們著重來看看調(diào)用生成器對象的 next 方法傳入?yún)?shù)時究竟會發(fā)生什么事情,理解 next() 方法的參數(shù)是后續(xù) Generator 解決異步的重點實現(xiàn)思路。
上文我們提到過,生成器函數(shù)中的 yield 關(guān)鍵字會暫停函數(shù)的運行,簡單來說比如我們第一次調(diào)用 g.next() 方法時函數(shù)會執(zhí)行到 yield 1 語句,此時函數(shù)會被暫停。
當(dāng)?shù)诙握{(diào)用 g.next() 方法時,生成器函數(shù)會繼續(xù)從上一次暫停的語句開始執(zhí)行。這里有一個需要注意的點:當(dāng)生成器函數(shù)恢復(fù)執(zhí)行時,因為上一次執(zhí)行到 const a = yield 1 語句的右半段并沒有給 const a進(jìn)行賦值。
那么此時的賦值語句 const a = yield 1,a 會被賦為什么值呢?細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了。我們在 g.next('param-a') 傳入的參數(shù) param-a 會作為生成器函數(shù)重新執(zhí)行時,上一次 yield 語句的返回值進(jìn)行執(zhí)行。
簡單來說,也就是調(diào)用 g.next('param-a')恢復(fù)函數(shù)執(zhí)行時,相當(dāng)于將生成器函數(shù)中的 const a = yield 1; 變成 const a = 'param-a'; 進(jìn)行執(zhí)行。
這樣,第二次調(diào)用 g.next('param-a')時自然就打印出了 param-a this is a 。
同樣當(dāng)我們第三次調(diào)用 g.next('param-b') 時,本次調(diào)用 next 函數(shù)傳入的參數(shù)會被當(dāng)作 yield 2 運算結(jié)果賦值給 b 變量,執(zhí)行到打印時會輸出 param-b this is b。
同理 g.next('paramc') 會輸出 param-c this is b。
總而言之,當(dāng)我們?yōu)?next 傳遞值進(jìn)行調(diào)用時,傳入的值會被當(dāng)作上一次生成器函數(shù)暫停時 yield 關(guān)鍵字的返回值處理。
自然,第一次調(diào)用 g.next() 傳入?yún)?shù)是毫無意義的。因為首次調(diào)用 next 函數(shù)時,生成器函數(shù)并沒有任何執(zhí)行自然也沒有 yield 關(guān)鍵字處理。
接下來我們來看看所謂的生成器函數(shù)返回值:
function*?gen()?{
??const?a?=?yield?1;
??console.log(a,?'this?is?a');
??const?b?=?yield?2;
??console.log(b,?'this?is?b');
??const?c?=?yield?3;
??console.log(c,?'this?is?c');
??return?'resultValue'
}
let?g?=?gen();
g.next();?//?{?value:?1,?done:?false?}
g.next('param-a');?//?{?value:?2,?done:?false?}
g.next('param-b')?//?{?value:?3,?done:?false?}
g.next()?//?{?value:?'resultValue',?done:?true?}
g.next()?//?{?value:?undefined,?done:?true?}
復(fù)制代碼
當(dāng)生成器函數(shù)存在 return 返回值時,我們會在第四次調(diào)用 g.next() 函數(shù)恢復(fù)執(zhí)行,此時生成器函數(shù)繼續(xù)執(zhí)行函數(shù)執(zhí)行完畢。
此時自然 done 會變?yōu)?true 表示生成器函數(shù)已經(jīng)執(zhí)行完畢,之后,由于函數(shù)存在返回值所以隨之本次的 value 會變?yōu)?'resultValue' 。
也就是當(dāng)生成器函數(shù)執(zhí)行完畢時,原本本次調(diào)用 next 方法返回的 {done:true,value:undefined} 變?yōu)榱?code style="font-size: 14px;border-radius: 4px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(155, 110, 35);background-color: rgb(255, 245, 227);padding: 3px;margin: 3px;">{ done:true,value:'resultValue'}。
關(guān)于 Generator 函數(shù)的基本使用我們就介紹到這里,接下來我們來看看它是如何被 JavaScript 實現(xiàn)的。
Generator 原理實現(xiàn)
關(guān)于 Generator 函數(shù)的原理其實和我們后續(xù)的異步關(guān)系并不是很大,但是本著“知其然,知其所以然”的出發(fā)點。
希望大家可以耐心去閱讀本小結(jié),其實它的內(nèi)部運行機(jī)制并不是很復(fù)雜。筆者自己也在某電商大廠面試中被問到過如何實現(xiàn) Generator 的 polyfill。
首先,你可以打開鏈接查看我已經(jīng)編輯好的 ?? Babel Generator Demo[6]。

乍一看也許很多同學(xué)會稍微有點懵,沒關(guān)系。這段代碼并不難,難的是你對未知恐懼的心態(tài)。
這是 Babel 在低版本瀏覽器下為我們實現(xiàn)的 Generator 生成器函數(shù)的 polyfill[7] 實現(xiàn)。
左側(cè)為 ES6 中的生成器語法,右側(cè)為轉(zhuǎn)譯后的兼容低版本瀏覽器的實現(xiàn)。
首先左側(cè)的 gen 生成器函數(shù)被在右側(cè)被轉(zhuǎn)化成為了一個簡單的普通函數(shù),具體 gen 函數(shù)內(nèi)容我們先忽略它。
在右側(cè)代碼中,對于普通 gen 函數(shù)包裹了一層 regeneratorRuntime.mark(gen) 處理,在源碼中這一步其實為了將普通 gen 函數(shù)繼承 GeneratorFunctionPrototype 從而實現(xiàn)將 gen() 返回的對象變成 Generator 實例對象的處理。
這一步對于我們理解 Generator 顯得并不是那么重要,所以我們可以簡單的將 regeneratorRuntime.mark 改寫成為這樣的結(jié)構(gòu):
//?自己定義regeneratorRuntime對象
const?regeneratorRuntime?=?{
????//?存在mark方法,接受傳入的fn。原封不懂的返回fn
????mark(fn)?{
????????return?fn
????}
}
復(fù)制代碼
我們自己定義了 regeneratorRuntime 對象,并且為他定義了一個 mark 方法。它的作用非常簡單,接受一個函數(shù)作為入?yún)⒉⑶曳祷睾瘮?shù),僅此而已。
之后我們再來進(jìn)入 gen 函數(shù)內(nèi)部,在左側(cè)源代碼中當(dāng)我們調(diào)用 gen() 時,是會返回一個 Iterator 對象(它擁有 next 方法,并且每次調(diào)用 next 都會返回 {value:VALUE,done:boolean})。
所以在右側(cè)我們了解到了,我們通過調(diào)用編譯后的普通 gen() 函數(shù)應(yīng)該和右側(cè)返回一致,所謂 regeneratorRuntime.wrap() 方法也應(yīng)該返回一個具有 next 屬性的迭代器對象。
關(guān)于 regeneratorRuntime.wrap() 方法,這里傳入了兩個參數(shù),第一個參數(shù)是一個接受傳入 context 的函數(shù),第二個參數(shù)是我們之前處理的 _marked 對象。
同樣關(guān)于 wrap() 方法的第二個參數(shù)我們不用過多糾結(jié),它仍然是對于編譯后的生成器函數(shù)作為繼承使用的一個參數(shù),并不影響函數(shù)的核心邏輯。所以我們暫時忽略它。
此時,我們了解到,regeneratorRuntime 對象上應(yīng)該存在一個 wrap 方法,并且 wrap 方法應(yīng)該返回一個滿足迭代器協(xié)議[8]的對象。
//?自己定義regeneratorRuntime對象
const?regeneratorRuntime?=?{
????//?存在mark方法,接受傳入的fn。原封不懂的返回fn
????mark(fn)?{
????????return?fn
??},
??wrap(fn)?{
????//?...
????return?{
??????next()?{
??????????done:?...,
??????????value:?...
??????}
????}
??}
}
復(fù)制代碼
讓我們進(jìn)入 regeneratorRuntime.wrap 內(nèi)部傳入的具體函數(shù)來看看:
function?gen()?{
??var?a,?b,?c;
??return?regeneratorRuntime.wrap(function?gen$(_context)?{
????//?while(1)?配合函數(shù)的return沒有任何實際意義
????//?通常在編程中使用?while(1)?來表示while中的內(nèi)部會被多次執(zhí)行
????while?(1)?{
??????switch?((_context.prev?=?_context.next))?{
????????case?0:
??????????_context.next?=?2;
??????????return?1;
????????case?2:
??????????a?=?_context.sent;
??????????console.log(a,?'this?is?a');
??????????_context.next?=?6;
??????????return?2;
????????case?6:
??????????b?=?_context.sent;
??????????console.log(b,?'this?is?b');
??????????_context.next?=?10;
??????????return?3;
????????case?10:
??????????c?=?_context.sent;
??????????console.log(c,?'this?is?c');
????????case?12:
????????case?'end':
??????????return?_context.stop();
??????}
????}
??},?_marked);
}
復(fù)制代碼
regeneratorRuntime.wrap 內(nèi)部傳入的函數(shù),使用了 while(1) 內(nèi)部邏輯,因為我們在 while 循環(huán)中配合了函數(shù)的 return 語句,所以這里 while(1) 其實并沒有任何含義。
通常,在編程中我們用 while(1) 來表示內(nèi)部的邏輯會被執(zhí)行很多次,的確在函數(shù)內(nèi)部的 while 循環(huán)每次調(diào)用 next 方法其實都會進(jìn)入這段邏輯執(zhí)行。
首先我們來看看 傳入的 _context 參數(shù)存在哪些屬性:
_context.prev 表示本次生成器函數(shù)執(zhí)行時的指針位置。
_context.next 表示下次生成器函數(shù)調(diào)用時的指針位置。
_context.sent 表示調(diào)用 g.next(params) 時,傳入的 params 參數(shù)。
_context.stop 表示當(dāng)調(diào)用 g.next() 生成器函數(shù)執(zhí)行完畢調(diào)用的方法。
在解釋了 _context 對象上的屬性含義之后,也許你還是不太明白它們各自的含義。我們先來看看簡化后的實現(xiàn)代碼:
const?regeneratorRuntime?=?{
??//?存在mark方法,接受傳入的fn。原封不懂的返回fn
??mark(fn)?{
????return?fn;
??},
??wrap(fn)?{
????const?_context?=?{
??????next:?0,?//?表示下一次執(zhí)行生成器函數(shù)狀態(tài)機(jī)switch中的下標(biāo)
??????sent:?'',?//?表示next調(diào)用時候傳入的值?作為上一次yield返回值
??????done:?false,?//?是否完成
??????//?完成函數(shù)
??????stop()?{
????????this.done?=?true;
??????},
????};
????return?{
??????next(param)?{
????????//?1.?修改上一次yield返回值為context.sent
????????_context.sent?=?param;
????????//?2.執(zhí)行函數(shù)?獲得本次返回值
????????const?value?=?fn(_context);
????????//?3.?返回
????????return?{
??????????done:?_context.done,
??????????value,
????????};
??????},
????};
??},
};
復(fù)制代碼
完整的 regeneratorRuntime 對象就像上邊實現(xiàn)的那樣,它看起來非常簡單對吧。
在 wrap 函數(shù)中,我們接受傳入的一個狀態(tài)機(jī)函數(shù)。每次調(diào)用 wrap() 方法返回的 next(param) 方法時,會將 next(param) 中傳入的參數(shù)傳遞給 wrap 函數(shù)中的維護(hù)的 _context.sent 作為模擬上一次 yield 返回值而出現(xiàn)。
同時在 wrap(fn) 中傳入的 fn 你可以將它看作一個小型狀態(tài)機(jī) ,每次調(diào)用 next() 方法都會執(zhí)行狀態(tài)機(jī) fn 函數(shù)。
不過,因為狀態(tài)機(jī)中每次執(zhí)行時 _context.prev 的值都是不同的造成了每次調(diào)用 next 函數(shù)都執(zhí)行的是狀態(tài)機(jī)中不同的邏輯。
直到,狀態(tài)機(jī)函數(shù) fn 中的 switch 語句匹配完成返回 _context.stop() 函數(shù),此時將 _context.done 變?yōu)?true 并且返回對應(yīng)對象。
所謂 Generator 核心代碼簡化后不過上述短短幾十行,它的內(nèi)部核心思想本質(zhì)上就是通過 regeneratorRuntime.wrap 函數(shù)包裹一個狀態(tài)機(jī)函數(shù) fn 。
wrap 函數(shù)內(nèi)部維護(hù)一個 _context 對象,從而每次調(diào)用返回的生成器對象的 next 方法時,被包裹的狀態(tài)機(jī)函數(shù)根據(jù) _context 的對應(yīng)屬性匹配對應(yīng)狀態(tài)來完成不同的邏輯。
Generator 核心原理其實就是這樣,有興趣了解完整代碼的同學(xué)可以查看 facebook/regenerator[9]。
Generator 異步解決方案
在講述完 Generator 的基礎(chǔ)概念和 polyfill 原理之后,我們來步入異步瞧瞧它是如何被應(yīng)用在異步編程中的。
大多數(shù)情況下,我們會直接使用 Promise 來處理異步問題。Promise 幫助我們解決了非常糟糕的“回調(diào)地獄”式的異步解決方案。
但是 Promise 中仍然需要存在不停的 .then .then 當(dāng)異步嵌套過多的情況下,Promise 中的 then 式調(diào)用也顯得不是那么一種直觀。
每當(dāng)問題的產(chǎn)生一定會伴隨解決方案的出現(xiàn),在 Promise 處理異步問題時,Generator 顯然作為解決方案出現(xiàn)在了我們的視野中。
function?promise1()?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('1');
????},?1000);
??});
}
function?promise2(value)?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('value:'?+?value);
????},?1000);
??});
}
function*?readFile()?{
??const?value?=?yield?promise1();
??const?result?=?yield?promise2(value);
??return?result;
}
復(fù)制代碼
我們來看看上述的代碼,readFile 函數(shù)是不是稍微有一些 async 函數(shù)的影子。
假如我期望所謂 readFile() 方法和 async 函數(shù)行為一致,返回的 result 同樣是一個 Promise 并且保持上訴代碼的寫法,我們應(yīng)該如何做?
看到這里,你可以稍微思考下應(yīng)該如何利用 Generator 函數(shù)的特性去實現(xiàn)。
上邊我們提到過生成器函數(shù)具有可暫停的特點,當(dāng)調(diào)用生成器函數(shù)后會返回一個生成器對象。每次調(diào)用生成器對象的 next 方法,生成器函數(shù)才會繼續(xù)往下執(zhí)行直到碰到下一個 yield 語句,同時每次調(diào)用生成器對象的 next(param) 方法時,我們可以傳入一個參數(shù)作為上一次 yield 語句的返回值。
利用上述特性,我們可以寫出如下的代碼:
function?promise1()?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('1');
????},?1000);
??});
}
function?promise2(value)?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('value:'?+?value);
????},?1000);
??});
}
function*?readFile()?{
??const?value?=?yield?promise1();
??const?result?=?yield?promise2(value);
??return?result;
}
function?asyncGen(fn)?{
??const?g?=?fn();?//?調(diào)用傳入的生成器函數(shù)?返回生成器對象
??//?期望返回一個Promise
??return?new?Promise((resolve)?=>?{
????//?首次調(diào)用?g.next()?方法,會執(zhí)行生成器函數(shù)直到碰到第一個yield關(guān)鍵字
????//?這里會執(zhí)行到?yield?promise1()?同時將?promise1()?返回為迭代器對象的?value?值
????const?{?value,?done?}?=?g.next();
????//?因為value為Promise?所以可以等待promise完成后,在then函數(shù)中繼續(xù)調(diào)用?g.next(res)?恢復(fù)生成器函數(shù)繼續(xù)執(zhí)行
????value.then((res)?=>?{
??????//?同時第二次調(diào)用g.next()?時是在上一次返回的promise.then中
??????//?我們可以拿到上一次Promise的value值也就是?'1'
??????//?傳入g.next('1')?作為上一次yield的值?這里相當(dāng)于?const?value?=?'1'
??????const?{?value,?done?}?=?g.next(res);
??????//?同理,繼續(xù)上述過程
??????value.then(resolve);
????});
??});
}
asyncGen(readFile).then((res)?=>?console.log(res));?//?value:?1
復(fù)制代碼
我們通過定義了一個 asyncGen 函數(shù)來包裹 readFile 生成器函數(shù),利用了生成器函數(shù)結(jié)合 yield 關(guān)鍵字可以被暫停的特點,同時結(jié)合 Promise.prototype.then 方法的特性實現(xiàn)了類似于 async 函數(shù)的異步寫法。
看上去它和 async 很像對吧,不過目前的代碼存在一個致命的問題:
asyncGen 函數(shù)并不具備通用性,上邊的例子中 readFile 函數(shù)內(nèi)部包裹了兩層 yield 處理的 promise,我們在 asyncGen 函數(shù)內(nèi)部同樣兩次調(diào)用 g.next() 方法。
如果我們包裹三層 yield 處理的 Promise ,那么我是不是重新書寫 asyncGen 函數(shù)邏輯。又或者 readFile 中存在比如 yield '1' 并不是 Promise 的語句,那么我們當(dāng)作 Promise 使用 then 方法處理肯定是會報錯。
這樣的方法不具備任何通用性,所以在實際項目中沒有人會這樣去組織異步代碼。但是通過這個例子我相信你已經(jīng)可以初步了解 Generator 生成器函數(shù)是如何結(jié)合 Promise 來作為異步解決方案的。
tj/co
上邊我們使用 Generator 作為異步解決方案,我們編寫了一個名為 asyncGen 的包裹函數(shù),可是它并不具備任何通用性。
接下來我們來思考如何讓這個方法變得更加通用,從而在各種不同場景下去更好的解決異步問題:
同樣,我希望我的 readFile 方法書寫時方式和之前一樣直觀:
function?promise1()?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('1');
????},?1000);
??});
}
function?promise2(value)?{
??return?new?Promise((resolve)?=>?{
????setTimeout(()?=>?{
??????resolve('value:'?+?value);
????},?1000);
??});
}
function*?readFile()?{
??const?value?=?yield?promise1();
??const?result?=?yield?promise2(value);
??return?result;
}
復(fù)制代碼
在這之前我們使用 Generator 的特性來處理 Promise 的異步問題,每次都傻傻的去根據(jù) yeild 關(guān)鍵字去嵌套函數(shù)邏輯處理。
一些同學(xué)可能之前就想到了,對于無窮無盡的嵌套調(diào)用邏輯同時又存在邊界停止條件。那么當(dāng)我們需要封裝出一個具有通用性的函數(shù)時,使用遞歸來處理不是更好嗎?
或許根據(jù)這個思路,你先可以嘗試自己封裝一下。
不賣關(guān)子了,我們來看看具有通用性且更加優(yōu)雅的 Generator 異步解決方案:
function?co(gen)?{
??return?new?Promise((resolve,?reject)?=>?{
????const?g?=?gen();
????function?next(param)?{
??????const?{?done,?value?}?=?g.next(param);
??????if?(!done)?{
????????//?未完成?繼續(xù)遞歸
????????Promise.resolve(value).then((res)?=>?next(res));
??????}?else?{
????????//?完成直接重置?Promise?狀態(tài)
????????resolve(value);
??????}
????}
????next();
??});
}
co(readFile).then((res)?=>?console.log(res));
復(fù)制代碼
我們定義了一個 co 函數(shù)來包裹傳入的 generator 生成器函數(shù)。
在函數(shù) co 中,我們返回了一個 Promise 來作為包裹函數(shù)的返回值,同時首次調(diào)用 co 函數(shù)時會調(diào)用 gen() 得到對應(yīng)的生成器對象。
之后我們定義了一次 next 方法,在 next 函數(shù)內(nèi)部只要迭代器未完成那么此時我們就會在 value 的 then 方法中在此遞歸調(diào)用該 next 函數(shù)。
其實關(guān)于異步迭代時,大多數(shù)情況下都可以使用類似該函數(shù)中的遞歸方式來處理。
函數(shù)中稍微有三點需要大家額外注意:
首先我們可以看到 next 函數(shù)接受傳入的一個 param 的參數(shù)。
這是因為我們使用 Generator 來處理異步問題時,通過 const a = yield promise 將 promise 的 resolve 值交給 a ,所以我們需要在每次 then 函數(shù)中將 res 傳遞給下一次的 next(res) 作為上次 yield 的返回值。
其次,細(xì)心的同學(xué)可以留意到這一句代碼 Promise.resolve(value).then((res) => next(res));。
我們使用 Promise.resolve 將 value 進(jìn)行了一層包裹,這是因為當(dāng)生成器函數(shù)中的 yield 方法后緊挨的并不是 Promise 時,此時我們需要統(tǒng)一當(dāng)作 Promise 來處理,因為我們需要統(tǒng)一調(diào)用 .then 方法。
最后,首次調(diào)用 next() 方法時,我們并沒有傳入 param 參數(shù)。
相信這個并不難理解,當(dāng)我們不傳入 param 時相當(dāng)于直接調(diào)用 g.next() ,上邊我們提到過當(dāng)調(diào)用生成器對象的 next 方法傳入?yún)?shù)時,該參數(shù)會當(dāng)作上一次 yield 語句的返回值來處理。
因為首次調(diào)用 g.next() 時,生成器函數(shù)內(nèi)部之前并不存在 yield ,所以傳入?yún)?shù)是沒有任何意義的。
它看來并不是很難對吧,但是通過這一小段代碼讓我們的 Generator 擁有了可以讓異步代碼同步調(diào)用的書寫方式來使用。
其實這一小段代碼也是所謂 co[10] 庫的核心原理,當(dāng)然所謂 co 遠(yuǎn)遠(yuǎn)不止這些,但是這段代碼足夠我們了解所謂在 Async/Await 未出現(xiàn)之前我們是如何使用所謂的 Generator 來作為終極異步解決方案了。
Async/Await
鋪墊了這么久終于來到現(xiàn)階段 JavaScript 解決異步的最終方案了。
在前邊我們聊到過所謂 Generator 的基礎(chǔ)用法以及 Babel 是如何在 EcmaScript 5 中使用 Generator 生成器。
同時我們順帶聊了下,在 Async 沒有出現(xiàn)之前我們?nèi)绾问褂?Generator 結(jié)合 Promise 來處理異步問題。
雖然之前的異步調(diào)用方式看起來已經(jīng)非常類似于 async 語法了:
function*?readFile()?{
??const?value?=?yield?promise1();
??const?result?=?yield?promise2(value);
??return?result;
}
復(fù)制代碼
但是在使用 readFile 時,我們?nèi)匀恍枰褂?co 函數(shù)來將 Generator 來單獨處理一層:
co(readFile).then((res)?=>?console.log(res));
復(fù)制代碼
那么此時,async 的出現(xiàn)解決了這一問題,照舊,我們先來看看在不支持 async 語法的低版本瀏覽器下 Babel 是如何處理它的:

乍一看可能信息有點多,別擔(dān)心。其實這些都是我們之前分析甚至實現(xiàn)過的代碼。
在深入這段代碼之前,我先告訴你所謂 Async 語法是如何被實現(xiàn)的結(jié)論:
在這之前,我們通過 Generator 和 Promise 解決異步問題時,需要將 Generator 函數(shù)額外使用 co 來包裹一層從而實現(xiàn)類似同步的異步函數(shù)調(diào)用。
那么如果我們想要實現(xiàn)低版本瀏覽器下的 Async 語法,那么我們將 co 函數(shù)伴隨 Generator 的 polyfill 一起編譯出來不就可以了嗎?
所謂 Async 其實就是將 Generator 包裹了一層 co 函數(shù),所以它被稱為 Generator 和 Promise 的語法糖。
接下來我們一起來看看右邊的 polyfil 實現(xiàn)。

這段函數(shù)是不是很眼熟,我們在之前簡單聊過關(guān)于它的實現(xiàn)原理。
唯一有一點不同的是,它將 Generator 的實現(xiàn)額外包裹了一層 _asyncToGenerator 函數(shù)進(jìn)行返回。
function?_asyncToGenerator(fn)?{
??return?function?()?{
????var?self?=?this,
??????args?=?arguments;
????return?new?Promise(function?(resolve,?reject)?{
??????var?gen?=?fn.apply(self,?args);
??????function?_next(value)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'next',?value);
??????}
??????function?_throw(err)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'throw',?err);
??????}
??????_next(undefined);
????});
??};
}
復(fù)制代碼
來看看所謂的 _asyncToGenerator 函數(shù),它內(nèi)部接受一個傳入的 fn 函數(shù)。這個 fn 正是所謂的 Generator 函數(shù)。
當(dāng)調(diào)用 hello() 時,實質(zhì)上最終相當(dāng)于調(diào)用 _asyncToGenerator 內(nèi)部的返回函數(shù),它會返回一個 Promise。
return?new?Promise(function?(resolve,?reject)?{
??????var?gen?=?fn.apply(self,?args);
??????function?_next(value)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'next',?value);
??????}
??????function?_throw(err)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'throw',?err);
??????}
??????_next(undefined);
????});
復(fù)制代碼
首先 Promise 中會進(jìn)行:
var?gen?=?fn.apply(self,?args);
復(fù)制代碼
它會調(diào)用我們傳入的 fn(生成器函數(shù)) 返回生成器對象,之后它定義了兩個方法:
function?_next(value)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'next',?value);
??????}
function?_throw(err)?{
?????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'throw',?err);
?????}
復(fù)制代碼
這兩個方法內(nèi)部都基于 asyncGeneratorStep 來進(jìn)行函數(shù)調(diào)用:
function?asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?key,?arg)?{
??try?{
????var?info?=?gen[key](arg);
????var?value?=?info.value;
??}?catch?(error)?{
????reject(error);
????return;
??}
??if?(info.done)?{
????resolve(value);
??}?else?{
????Promise.resolve(value).then(_next,?_throw);
??}
}
復(fù)制代碼
所謂 asyncGeneratorStep 其實你完全可以將它當(dāng)作我們之前實現(xiàn)的異步解決方案 co 的原理,它們內(nèi)部的思想和實現(xiàn)是完全一致的,唯一的區(qū)別就是這里 Babel 編譯后的實現(xiàn)考慮了 error 情況而我們當(dāng)時并沒有考慮出現(xiàn)錯誤時的情況。
本質(zhì)上還是利用 Generator 函數(shù)內(nèi)部可以被暫停執(zhí)行的特性結(jié)合 Promise.prototype.then 中進(jìn)行遞歸調(diào)用從而實現(xiàn) Async 的語法糖。
其實看到這里,經(jīng)過前邊知識點的鋪墊我相信最終 Async/Await 的實現(xiàn)原理對你來說一點都不會陌生。
之所以被稱為語法糖因為本質(zhì)上它也并沒有任何針對于 Generator 生成器函數(shù)和 Promise 的其他知識拓展點,恰恰是更好的結(jié)合了這兩種語法的特點衍生出了更加優(yōu)雅簡單的異步解決方案。
結(jié)尾
文章的結(jié)尾,首先感謝每一個可以讀到這里的朋友。
我們講述了從 Generator 函數(shù)發(fā)展到 Async/Await 的異步解決方案以及它們是如何在低版本瀏覽器中的 polyfill 最終延伸到它們的實現(xiàn)原理。
其實文章中的很多代碼都是精簡的實現(xiàn)版本,如果對哪個步驟閱讀完文章還存在疑惑的話你也可以在評論區(qū)留下你的見解我們共同探討,或者你可以去查看每個章節(jié)末尾對應(yīng)的源碼地址。
關(guān)于本文
作者:19組清風(fēng)
https://juejin.cn/post/7069317318332907550
Node 社群
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:
1. 點個「在看」,讓更多人也能看到這篇文章 2. 訂閱官方博客?www.inode.club?讓我們一起成長 點贊和在看就是最大的支持??
