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

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

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

這段函數是不是很眼熟,我們在之前簡單聊過關于它的實現原理。
唯一有一點不同的是,它將 Generator 的實現額外包裹了一層 _asyncToGenerator 函數進行返回。
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);
????});
??};
}
復制代碼
來看看所謂的 _asyncToGenerator 函數,它內部接受一個傳入的 fn 函數。這個 fn 正是所謂的 Generator 函數。
當調用 hello() 時,實質上最終相當于調用 _asyncToGenerator 內部的返回函數,它會返回一個 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);
????});
復制代碼
首先 Promise 中會進行:
var?gen?=?fn.apply(self,?args);
復制代碼
它會調用我們傳入的 fn(生成器函數) 返回生成器對象,之后它定義了兩個方法:
function?_next(value)?{
????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'next',?value);
??????}
function?_throw(err)?{
?????????asyncGeneratorStep(gen,?resolve,?reject,?_next,?_throw,?'throw',?err);
?????}
復制代碼
這兩個方法內部都基于 asyncGeneratorStep 來進行函數調用:
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);
??}
}
復制代碼
所謂 asyncGeneratorStep 其實你完全可以將它當作我們之前實現的異步解決方案 co 的原理,它們內部的思想和實現是完全一致的,唯一的區(qū)別就是這里 Babel 編譯后的實現考慮了 error 情況而我們當時并沒有考慮出現錯誤時的情況。
本質上還是利用 Generator 函數內部可以被暫停執(zhí)行的特性結合 Promise.prototype.then 中進行遞歸調用從而實現 Async 的語法糖。
其實看到這里,經過前邊知識點的鋪墊我相信最終 Async/Await 的實現原理對你來說一點都不會陌生。
之所以被稱為語法糖因為本質上它也并沒有任何針對于 Generator 生成器函數和 Promise 的其他知識拓展點,恰恰是更好的結合了這兩種語法的特點衍生出了更加優(yōu)雅簡單的異步解決方案。
結尾
文章的結尾,首先感謝每一個可以讀到這里的朋友。
我們講述了從 Generator 函數發(fā)展到 Async/Await 的異步解決方案以及它們是如何在低版本瀏覽器中的 polyfill 最終延伸到它們的實現原理。
其實文章中的很多代碼都是精簡的實現版本,如果對哪個步驟閱讀完文章還存在疑惑的話你也可以在評論區(qū)留下你的見解我們共同探討,或者你可以去查看每個章節(jié)末尾對應的源碼地址。
關于本文
作者:19組清風
https://juejin.cn/post/7069317318332907550

往期推薦



最后
歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...


