小哥哥小姐姐,来尝尝 Async 函数这块语法糖
編者注:眾所周知,JS 最大的特性就是異步,異步提高了性能但是卻給我們編寫帶來了一定困難,造就了令人發(fā)指的回調(diào)地獄。為了解決這個問題,一個又一個的解決方案被提出來。今天我們請來了 《JavaScript 高級程序設(shè)計》等多本書的知名譯者 @李松峰 老師給我們講解下各種異步函數(shù)編寫的解決方案以及各種內(nèi)涵。
本次內(nèi)容是基于之前分享的文字版,若想看重點的話可以看之前的 PPT:ppt.baomitu.com/d/fd045abb
也可以查看之前的分享視頻:cloud.live.360vcloud.net/theater/pla…
ES7(ECMAScript 2016)推出了Async函數(shù)(async/await),實現(xiàn)了以順序、同步代碼的編寫方式來控制異步流程,徹底解決了困擾JavaScript開發(fā)者的“回調(diào)地獄”問題。比如,之前需要嵌套回調(diào)的異步邏輯:
const result = []; // pseudo-code, ajax stand for an asynchronous request ajax('url1', function(err, data){if(err) {...}result.push(data)ajax('url2', function(err, data){if(err) {...}result.push(data)console.log(result)}) }) 復(fù)制代碼現(xiàn)在可以寫成如下同步代碼的樣式了:
async function example() {const r1 = await new Promise(resolve =>setTimeout(resolve, 500, 'slowest'))const r2 = await new Promise(resolve =>setTimeout(resolve, 200, 'slow'))return [r1, r2] }example().then(result => console.log(result)) // ['slowest', 'slow'] 復(fù)制代碼Async函數(shù)需要在function前面添加async關(guān)鍵字,同時內(nèi)部以await關(guān)鍵字來“阻塞”異步操作,直到異步操作返回結(jié)果,然后再繼續(xù)執(zhí)行。在沒有Async函數(shù)以前,我們無法想象下面的異步代碼可以直接拿到結(jié)果:
const r1 = ajax('url') console.log(r1) // undefined 復(fù)制代碼這當(dāng)然是不可能的,異步函數(shù)的結(jié)果只能在回調(diào)里拿到。可以說,Async函數(shù)是JavaScript程序員在探索如何高效異步編程過程中踩“坑”之后的努力“自救”獲得的成果——不是“糖果”。然而,讀者小哥哥小姐姐可能有所不知,Async函數(shù)實際上是一個語法糖(果然是“糖果”嗎?),它的背后是ES6(ECMAScript 2015)中推出的Promise、Iterator和Generator,我們簡稱“PIG”。本文就帶各位好好品嘗品嘗這塊語法糖,感受一個PIG是如何成就Async函數(shù)的。
1. 當(dāng)前JavaScript編程主要是異步編程
當(dāng)前JavaScript編程主要是異步編程。為什么這么說呢?網(wǎng)頁或Web開發(fā)最早從2005年Ajax流行開始,逐步向重交互時代邁進(jìn)。特別是SPA(Single Page Application,單頁應(yīng)用)流行之后,一度有人提出“Web頁面要轉(zhuǎn)向Web應(yīng)用,而且要媲美原生應(yīng)用”。如今在前端開發(fā)組件化的背景下催生的Angular、React和Vue,都是SPA進(jìn)一步演化的結(jié)果。
Web應(yīng)用或開發(fā)重交互的特征越來越明顯,意味著什么?意味著按照瀏覽器這個運行時的特性,頁面在首次加載過程中,與JavaScript相關(guān)的主要任務(wù)就是加載基礎(chǔ)運行庫和擴展庫(包括給低版本瀏覽器打補丁的腳本),然后初始化和設(shè)置頁面的狀態(tài)。首次加載之后,用戶對頁面的操作、數(shù)據(jù)I/O以及DOM更新,就全部交由異步JavaScript腳本管理。所以,目前JavaScript編程最大的應(yīng)用是Web交互,而Web交互的核心就是異步邏輯。
然而,ES6之前JavaScript中控制異步流程的手段只有事件和回調(diào)。比如下面的示例展示了通過原生XMLHttpRequest對象發(fā)送異步請求,然后給onload和onerror事件分別注冊成功和錯誤處理函數(shù):
var req = new XMLHttpRequest(); req.open('GET', url);req.onload = function () {if (req.status == 200) {processData(req.response);} };req.onerror = function () {console.log('Network Error'); };req.send(); 復(fù)制代碼下面的代碼展示了Node.js經(jīng)典的“先傳錯誤”的回調(diào)。但這里要重點提一下,這種函數(shù)式編程風(fēng)格也叫CPS,即Continuation Passing Style,我翻譯成“后續(xù)操作傳遞風(fēng)格”。因為調(diào)用readFile傳入了表示后續(xù)操作的一個回調(diào)函數(shù)。這一塊就不展開了。
// Node.js fs.readFile('file.txt', function (error, data) {if (error) {// ...}console.log(data);} ); 復(fù)制代碼事件和回調(diào)有很多問題,主要是它們只適用于簡單的情況。邏輯一復(fù)雜,代碼的編寫和維護(hù)成本就成倍上升。比如,大家熟知的“回調(diào)地獄”。更重要的是,回調(diào)模式的異步本質(zhì)與人類同步、順序的思維模式是相悖的。
為了應(yīng)對越來越復(fù)雜的異步編程需求,ES6推出了解決上述問題的Promise。
2. Promise
Promise,人們普遍的理解就是:“Promise是一個未來值的占位符”。也就是說,從語義上講,一個Promise對象代表一個對未來值的“承諾”(promise),這個承諾將來如果“兌現(xiàn)”(fulfill),就會“解決”(resolve)為一個有意義的數(shù)據(jù);如果“拒絕”(reject),就會“解決”為一個“拒絕理由”(rejection reason),就是一個錯誤消息。
Promise對象的狀態(tài)很簡單,一生下來的狀態(tài)是pending(待定),將來兌現(xiàn)了,狀態(tài)變成fulfilled;拒絕了,狀態(tài)變成rejected。fulfilled和rejected顯然是一種“確定”(settled)狀態(tài)。以上狀態(tài)轉(zhuǎn)換是不可逆的,所以Promise很單純,好控制,哈哈。
以下是Promise相關(guān)的所有API。前3個是創(chuàng)建Promise對象的(稍后有例子),后4個中的前2個是用于注冊反應(yīng)函數(shù)的(稍后有例子),后2個是用于控制并發(fā)和搶占的:
以下是通過Prmoise(executor)構(gòu)造函數(shù)創(chuàng)建Promise實例的詳細(xì)過程:要傳入一個“執(zhí)行函數(shù)”(executor),這個執(zhí)行函數(shù)又接收兩個參數(shù)“解決函數(shù)”(resolver)和“拒絕函數(shù)”(rejector),代碼中分別對應(yīng)變量resolve和reject,作用分別是將新建對象的狀態(tài)由pending改為fulfilled和rejected,同時返回“兌現(xiàn)值”(fulfillment)和“拒絕理由”(rejection)。當(dāng)然,resolve和reject都是在異步操作的回調(diào)中調(diào)用的。調(diào)用之后,運行時環(huán)境(瀏覽器引擎或Node.js的libuv)中的事件循環(huán)調(diào)度機制會把與之相關(guān)的反應(yīng)函數(shù)——兌現(xiàn)反應(yīng)函數(shù)或拒絕反應(yīng)函數(shù)以及相關(guān)的參數(shù)添加到“微任務(wù)”隊列,以便下一次“循檢”(tick)時調(diào)度到JavaScript線程去執(zhí)行。
如前所述,Promise對象的狀態(tài)由pending變成fulfilled,就會執(zhí)行“兌現(xiàn)反應(yīng)函數(shù)”(fulfillment reaction);而變成rejected,就會執(zhí)行“拒絕反應(yīng)函數(shù)”(rejection reaction)。如下例所示,常規(guī)的方式是通過p.then()注冊兌現(xiàn)函數(shù),通過p.catch()注冊拒絕函數(shù):
p.then(res => { // 兌現(xiàn)反應(yīng)函數(shù)// res === 'random success' }) p.catch(err => { // 拒絕反應(yīng)函數(shù)// err === 'random failure' }) 復(fù)制代碼當(dāng)然還有非常規(guī)的方式,而且有時候非常規(guī)方式可能更好用:
// 通過一個.then()方法同時注冊兌現(xiàn)和拒絕函數(shù) p.then(res => {// handle response},err => {// handle error} ) // 通過.then()方法只注冊一個函數(shù):兌現(xiàn)函數(shù) p.then(res => {// handle response }) // 通過.then()方法只傳入拒絕函數(shù),兌現(xiàn)函數(shù)的位置傳null p.then(null, err => {// handle error }) 復(fù)制代碼關(guān)于Promise就這樣吧。ES6除了Promise,還推出了Iterator(迭代器)和Generator(生成器),于是就有成就Async函數(shù)的PIG組合。下面我們分別簡單看一看Iterator和Generator。
3. Iterator
要理解Iterator或者迭代器,最簡單的方式是看它的接口:
interface IteratorResult {done: boolean;value: any; } interface Iterator {next(): IteratorResult; } interface Iterable {[Symbol.iterator](): Iterator } 復(fù)制代碼先從中間的Iterator看。
什么是迭代器?它是一個對象,有一個next()方法,每次調(diào)用next()方法,就會返回一個迭代器結(jié)果(看第一個接口IteratorResult)。而這個迭代器結(jié)果,同樣還是一個對象,這個對象有兩個屬性:done和value,其中done是一個布爾值,false表示迭代器迭代的序列沒有結(jié)束;true表示迭代器迭代的序列結(jié)束了。而value就是迭代器每次迭代真正返回的值。
再看最后一個接口Iterable,翻譯成“可迭代對象”,它有一個[Symbol.iterator]()方法,這個方法會返回一個迭代器。
可以結(jié)合前面的接口定義和下面這張圖來理解可迭代對象(實現(xiàn)了“可迭代協(xié)議”)、迭代器(實現(xiàn)了“迭代器協(xié)議”)和迭代器結(jié)果這3個簡單而又重要的概念(暫時理解不了也沒關(guān)系,后面還有一個無窮序列的例子,可以幫助大家理解)。
可迭代對象是一個我們非常熟悉的概念,數(shù)組、字符串以及ES6新增的集合類型Set和Map都是可迭代對象。這意味著什么呢?意味著我們可以通過E6新增的3個用于操作可迭代對象的語法:
- for...of
- [...iterable]
- Array.from(iterable)
注意 E6以前就有的以下語法不適用于可迭代對象:
- for...in
- Array#forEach
接下來我們看例子。
for (const item of sequence) {console.log(item)// 'i'// 't'// 'e'// 'r'// 'a'// 'b'// 'l'// 'e' }console.log([...sequence]) // ['i', 't', 'e', 'r', 'a', 'b', 'l', 'e']console.log(Array.from(sequence)) // ['i', 't', 'e', 'r', 'a', 'b', 'l', 'e'] 復(fù)制代碼以上示例分別使用for...of、擴展操作符(...)和Array.from()方法來迭代了前面定義的sequence這個可迭代對象。
下面再看一個通過迭代器創(chuàng)建無窮序列的小例子,通過這個例子我們再來深入理解與迭代器相關(guān)的概念。
const random = {[Symbol.iterator]: () => ({next: () => ({ value: Math.random() })}) }// 運行這行代碼會怎么樣? [...random] // 這行呢? Array.from(random) 復(fù)制代碼這個例子使用兩個ES6的箭頭函數(shù)定義了兩個方法,創(chuàng)建了三個對象。
最內(nèi)層的對象{ value: Math.random() }很明顯是一個“迭代器結(jié)果”(IteratorResult)對象,因為它有一個value屬性和一個……,等等,done屬性呢?這里沒有定義done屬性,所以每次迭代(調(diào)用next())時訪問IteratorResult.done都會返回false;所以這個迭代器結(jié)果的定義相當(dāng)于{ value: Math.random() , done: false }。顯然,done永遠(yuǎn)不可能是true,所以這是一個無窮隨機數(shù)序列!
interface IteratorResult {done: boolean;value: any; } 復(fù)制代碼再往外看,返回這個迭代器結(jié)果對象的箭頭函數(shù)被賦值給了外層對象的next()方法。根據(jù)Iterator接口的定義,如果一個對象包含一個next()方法,而這個方法的返回值又是一個迭代器結(jié)果,那么這個對象是什么?沒錯,就是迭代器。好,第二個對象是一個迭代器!
interface Iterator {next(): IteratorResult; } 復(fù)制代碼再往外看,返回這個迭代器對象的箭頭函數(shù)被賦值給了外層對象的[Symbol.iterator]()方法。根據(jù)Iterable接口的定義,如果一個對象包含一個[Symbol.iterator]()方法,而這個方法的返回值又是一個迭代器,那么這個對象是什么?沒錯,就是可迭代對象。
interface Iterable {[Symbol.iterator](): Iterator } 復(fù)制代碼好,到現(xiàn)在我們應(yīng)該徹底理解迭代器及其相關(guān)概念了。下面繼續(xù)看例子。前面的例子定義了一個可迭代對象random,這個對象的迭代器可以無限返回隨機數(shù),所以:
// 運行這行代碼會怎么樣? [...random] // 這行呢? Array.from(random) 復(fù)制代碼是的,這兩行代碼都會導(dǎo)致程序(或運行時)崩潰!因為迭代器會不停地運行,阻塞JavaScript執(zhí)行線程,最終可能因占滿可用內(nèi)存導(dǎo)致運行時停止響應(yīng),甚至崩潰。
那么訪問無窮序列的正確方式是什么?答案是使用解構(gòu)賦值或給for...of循環(huán)設(shè)置退出條件:
const [one, another] = random // 解析賦值,取得前兩個隨機數(shù) console.log(one) // 0.23235511826351285 console.log(another) // 0.28749457537196577for (const value of random) {if (value > 0.8) { // 退出條件,隨機數(shù)大于0.8則中斷循環(huán)break}console.log(value) } 復(fù)制代碼當(dāng)然,使用無窮序列還有更高級的方式,鑒于本文的目的,在此就不多介紹了。下面我們再說最后一個ES6的特性Generator。
4. Generator
依例,上接口:
interface Generator extends Iterator {next(value?: any): IteratorResult;[Symbol.iterator](): Iterator;throw(exception: any); } 復(fù)制代碼能看來出生成器是什么嗎?僅從它的接口來看,它既是一個迭代器,又是一個可迭代對象。沒錯,生成器因此又是迭代器的“加強版”,為什么?因為生成器還提供了一個關(guān)鍵字yield,它返回的序列值會自動包裝在一個IteratorResult(迭代器結(jié)果)對象中,省去了我們手工編寫相應(yīng)代碼的麻煩。下面就是一個生成器函數(shù)的定義:
function *gen() {yield 'a'yield 'b'return 'c' } 復(fù)制代碼哎,接口定義的生成器不是一個對象嗎,怎么是一個函數(shù)啊?
實際上,說生成器是對象或是函數(shù)都不確切。但我們知道,調(diào)用生成器函數(shù)會返回一個迭代器(接口描述的就是這個對象),這個迭代器可以控制返回它的生成器函數(shù)封裝的邏輯和數(shù)據(jù)。從這個意義上說,生成器由生成器函數(shù)及其返回的迭代器兩部分組成。再換句話說,生成器是一個籠統(tǒng)的概念,是一個統(tǒng)稱。(別急,一會你就明白這樣理解生成器的意義何在了。)
本節(jié)剛開始說了,生成器(返回的對象)“既是一個迭代器,又是一個可迭代對象”。下面我們就來驗證一下:
const chars = gen() typeof chars[Symbol.iterator] === 'function' // chars是可迭代對象 typeof chars.next === 'function' // chars是迭代器 chars[Symbol.iterator]() === chars // chars的迭代器就是它本身 console.log(Array.from(chars)) // 可以對它使用Array.from // ['a', 'b'] console.log([...chars]) // 可以對它使用Array.from // ['a', 'b'] 復(fù)制代碼通過代碼中的注釋我們得到了全部答案。這里有個小問題:“為什么迭代這個生成器返回的序列值中不包含字符'c'呢?”
原因在于,yield返回的迭代器結(jié)果對象的done屬性值都為false,所以'a'和'b'都是有效的序列值;而return返回的雖然也是迭代器結(jié)果對象,但done屬性的值卻是true,true表示序列結(jié)束,所以'c'不會包含在迭代結(jié)果中。(如果沒有return語句,代碼執(zhí)行到生成器函數(shù)末尾,會隱式返回{ value: undefined, done: true}。相信這一點不說你也知道。)
以上只是生成器作為“加強版”迭代器的一面。接下來,我們要接觸生成器真正強大的另一面了!
生成器真正強大的地方,也是它有別于迭代器的地方,在于它不僅能在每次迭代返回值,而且還能接收值。(當(dāng)然,生成器的概念里本身就有生成器函數(shù)嘛!函數(shù)當(dāng)然可以接收參數(shù)嘍。)等等,可不僅僅是可以給生成器函數(shù)傳參,而是還可以給yield表達(dá)式傳參!
function *gen(x) {const y = x * (yield)return y }const it = gen(6) it.next() // {value: undefined, done: false} it.next(7) // {value: 42, done: true} 復(fù)制代碼在上面這個簡單的生成器的例子中。我們定義了一個生成器函數(shù)*gen(),它接收一個參數(shù)x。函數(shù)體內(nèi)只有一個yield表達(dá)式,好像啥也沒干。但是,yield表達(dá)式似乎是一個“值的占位符”,因為代碼在某個時刻會計算變量x與這個“值”的乘積,并把該乘積賦值給變量y。最后,函數(shù)返回y。
這有點費解,下面我們一步一步分析。
這個例子中只有一個yield,假如還有更多的yield,則第4步會到第二個yield處再次暫停生成器函數(shù)的執(zhí)行,返回一個值,之后重復(fù)第3、4步,即還可以通過再調(diào)用it.next()向生成器函數(shù)中傳入值。
我們簡單總結(jié)一下,每次調(diào)用it.next(),可能有下列4種情況導(dǎo)致生成器暫停或停止執(zhí)行:
注意 這里的return和throw既可以在生成器函數(shù)內(nèi)部調(diào)用,也可以在生成器函數(shù)外部通過生成器的迭代器調(diào)用,比如:it.return(0)、it.throw(new Error('Oops'))。后面我們會給出相應(yīng)的例子。
由此,我們了解到,生成器的獨到之處就在于它的yield關(guān)鍵字。這個yield有兩大神奇之處:一、它是生成器函數(shù)暫停和恢復(fù)執(zhí)行的分界點;二、它是向外和向內(nèi)傳值(包括錯誤/異常)的媒介。
提到錯誤/異常,下面我們就來重點看一看生成器如何處理異常。畢竟,錯誤處理是使用回調(diào)方式編寫異步代碼的時候最讓JavaScript程序員頭疼的地方之一。
4.1 同步錯誤處理
首先,我們看“由內(nèi)而外”的錯誤傳遞,即從生成器函數(shù)內(nèi)部把錯誤拋到迭代器代碼中。
function *main() {const x = yield "Hello World";yield x.toLowerCase(); // 導(dǎo)致異常! }const it = main(); it.next().value; // Hello World try {it.next( 42 ); } catch (err) {console.error(err); // TypeError } 復(fù)制代碼如代碼注釋所提示的,生成器函數(shù)的第二行代碼會導(dǎo)致異常(至于為什么,讀者可以自己“人肉”執(zhí)行代碼,推演一下)。由于生成器函數(shù)內(nèi)部沒有做異常處理,因此錯誤被拋給了生成器的迭代代碼,也就是it.next(42)這行代碼。好在這行代碼被一個try/catch包著,錯誤可以正常捕獲并處理。
接下來,再看“由外而內(nèi)”(準(zhǔn)確地說,應(yīng)該是“由外而內(nèi)再而外”)的錯誤傳遞。
function *main() {var x = yield "Hello World";console.log('never gets here'); }const it = main(); it.next().value; // Hello World try {it.throw('Oops'); // `*main()`會處理嗎? } catch (err) { // 沒有!console.error(err); // Oops } 復(fù)制代碼如代碼所示,迭代代碼通過it.throw('Oops')拋出異常。這個異常是拋到生成器函數(shù)內(nèi)的(通過迭代器it)。拋進(jìn)去之后,yield表達(dá)式發(fā)現(xiàn)自己收到一個“燙手的山芋”,看看周圍也沒有異常處理邏輯“護(hù)駕”,于是眼疾手快,迅速又把這個異常給拋了出來。迭代器it顯然是有準(zhǔn)備的,它本意也是想先看看生成器函數(shù)內(nèi)部有沒有邏輯負(fù)責(zé)異常處理(看注釋“ // *main()會處理嗎?”),“沒有!”,它自己的try/catch早已等候多時了。
4.2 異步迭代生成器
前面我們看到的對生成器的迭代傳值,包括傳遞錯誤,都是同步的。實際上,生成器的yield表達(dá)式真正(哦,又一個“真正”)強大的地方在于:它在暫停生成器代碼執(zhí)行以后,不是必須等待迭代器代碼同步調(diào)用it.next()方法給它返回值,而是可以讓迭代器在一個異步操作的回調(diào)中取得返回值,然后再通過it.next(res)把值傳給它。
明白了嗎?yield可以等待一個異步操作的結(jié)果。從而讓本文開始提到的這種看似不可能的情況變成可能:
const r1 = ajax('url') console.log(r1) // undefined 復(fù)制代碼怎么變呢,在異步操作前加個yield呀:
const r1 = yield ajax('url') console.log(r1) // 這次r1就是真正的響應(yīng)結(jié)果了 復(fù)制代碼我們還是以一個返回Promise的異步操作為例來說明這一點比較好。因為基于回調(diào)的異步操作,很容易可以轉(zhuǎn)換成基于Promise的異步操作(比如jQuery的$.ajax()或通過util.promisify把Node.js中的異步方法轉(zhuǎn)換成Promise)。
例子來了。這是一個純Promise的例子。
function foo(x,y) {return request("http://some.url.1/?x=" + x + "&y=" + y); }foo(11, 31).then(function(text){console.log(text);},function(err){console.error(err);} ); 復(fù)制代碼函數(shù)foo(x, y)封裝了一個異步request請求,返回一個Promise。調(diào)用foo(11, 31)傳入?yún)?shù)后,request就向拼接好的URL發(fā)送請求,返回待定(pending)狀態(tài)的Promise對象。請求成功,則執(zhí)行then()中注冊的兌現(xiàn)反應(yīng)函數(shù),處理響應(yīng);請求失敗,則執(zhí)行拒絕反應(yīng)函數(shù),處理錯誤。
接下來我們要做的,就是將上面的代碼與生成器結(jié)合,讓生成器只關(guān)注發(fā)送請求和取得響應(yīng)結(jié)果,而把異步操作的等待和回調(diào)處理邏輯作為實現(xiàn)細(xì)節(jié)抽象出來。(“作為細(xì)節(jié)”,對,我們的目標(biāo)是只關(guān)注請求和結(jié)果,過程嘛,都是細(xì)節(jié),哈哈~。)
function foo(x, y) {return request("http://some.url.1/?x=" + x + "&y=" + y); } function *main() {try {const result = yield foo(11, 31); // 異步函數(shù)調(diào)用!console.log( result );} catch (err) {console.error( err );} } const it = main(); const p = it.next().value; // 啟動生成器并取得Promise `p`p.then( // 等待Promise `p`解決function(res){it.next(res); // 把`text`傳給`*main()`并恢復(fù)其執(zhí)行},function(err){it.throw(err); // 把`err`拋到`*main()`} ); 復(fù)制代碼注意,生成器函數(shù)(*main)的yield表達(dá)式中出現(xiàn)了異步函數(shù)調(diào)用:foo(11, 31)。而我們就要做的,就是在迭代器代碼中通過it.next()拿到這個異步函數(shù)調(diào)用返回的Promise,然后正確地處理它。怎么處理?我們看代碼。
創(chuàng)建生成器的迭代器之后,const p = it.next().value;返回了Promise p。在p的兌現(xiàn)反應(yīng)函數(shù)中,我們把拿到的響應(yīng)res通過it.next(res)調(diào)用傳回了生成器函數(shù)中的yield。yield拿到響應(yīng)結(jié)果res之后,立即恢復(fù)生成器代碼的執(zhí)行,把res賦值給變量result。于是,我們成功地在生成器函數(shù)中,以同步代碼的書寫方式取得了異步請求的響應(yīng)結(jié)果!神奇不?
(當(dāng)然,如果異步請求發(fā)生錯誤,在p的拒絕反應(yīng)函數(shù)中也會通過it.throw(err)把錯誤拋給生成器函數(shù)。但這個現(xiàn)在不重要。)
好啦,目標(biāo)達(dá)成:我們利用生成器的同步代碼,實現(xiàn)了對異步操作的完美控制。然而,還有一個問題。上面例子中的生成器只包裝了一個異步操作,如果是多個異步操作怎么辦呢?這時候,最好有一段通用的用于處理生成器函數(shù)的代碼,無論其中包含多少異步操作,這段代碼都能自動完成對Promise的接收、等待和響應(yīng)/錯誤傳遞等這些“細(xì)節(jié)”工作。
那不就是一個基于Promise的生成器運行程序嗎?
5. 通用的生成器運行程序
綜前所述,我們想要的是這樣一個結(jié)果:
function example() {return run(function *() {const r1 = yield new Promise(resolve =>setTimeout(resolve, 500, 'slowest'))const r2 = yield new Promise(resolve =>setTimeout(resolve, 200, 'slow'))return [r1, r2]}) }example().then(result => console.log(result)) // ['slowest', 'slow'] 復(fù)制代碼即定義一個通用的運行函數(shù)run,它負(fù)責(zé)處理傳給它的生成器函數(shù)中包裝的任意多個異步操作。針對每個操作,它都會正確地返回異步結(jié)果,或者向生成器函數(shù)中拋出異常。而運行這個函數(shù)的最終結(jié)果,也是返回一個Promise,這個Promise包含生成器函數(shù)返回的所有異步操作的結(jié)果(上例)。
已經(jīng)有聰明人實現(xiàn)了這樣的運行程序,下面我們就給出兩個實現(xiàn),大家可以自己嘗試去運行一下,然后“人肉”執(zhí)行,加深理解。
注意 在ES7推出Async函數(shù)之前,飽受回調(diào)之苦的JavaScript程序員就是靠類似的運行程序結(jié)合生成器給自己“續(xù)命”的。事實上,在ES6之前(沒有Promise、沒有生成器)的“蠻荒時代”,不屈不撓又足智多謀的JavaScript程序員們就已經(jīng)摸索出/找到了Thenable(Promise的前身)和類似生成器的實現(xiàn)方法(比如regenerator),讓瀏覽器能支持自己以同步風(fēng)格編寫異步代碼的高效干活兒夢。
苦哉!偉哉!悲夫,絞兮乎!
這是一個:
function run(gen) {const it = gen();return Promise.resolve().then( function handleNext(value){let next = it.next( value );return (function handleResult(next){if (next.done) {return next.value;} else {return Promise.resolve( next.value ).then(handleNext,function handleErr(err) {return Promise.resolve(it.throw( err )).then( handleResult );});} // if...else})(next); // handleResult(next)}); // handleNext(value) } 復(fù)制代碼供參考的“人肉”執(zhí)行過程
(調(diào)用run的代碼見本節(jié)開頭。)
這個run函數(shù)接收一個生成器函數(shù)作為參數(shù),然后立即創(chuàng)建了生成器的迭代器it(看上面run函數(shù)的代碼)。
然后,它返回一個Promise,是通過Promise.resolve()直接創(chuàng)建的。
我們給這個Promise的.then()方法傳入了一個兌現(xiàn)反應(yīng)函數(shù)(這個函數(shù)一定會被調(diào)用,因為Promise是兌現(xiàn)的),名叫handleNext(value),它接收一個參數(shù)value。第一次調(diào)用時,不會傳入任何值,因此value的值是undefined。
接下來,第一次調(diào)用it.next(value)啟動生成器,傳入undefined。生成器的第一個yield會返回一個待定狀態(tài)的Promise,至少500ms之后才會解決。
此時變量next的值是{ value: < Promise?[pending]>, done: false}。
接著,把next傳給下面的IIFE(Immediately Invoked Function Expression,立即調(diào)用函數(shù)表達(dá)式),這個函數(shù)叫handleResult(處理結(jié)果)。
在handleResult(next)內(nèi)部,首先檢查next.done,不等于true,進(jìn)入else子句。此時通過Promise.resolve(next.value)包裝next.value:等待返回的Promise解決,解決之后拿到字符串值'Slowest',然后傳給兌現(xiàn)反應(yīng)函數(shù)handleNext(value)。
至此,第一個異步操作的前半程處理完畢。接著,再次調(diào)用handleNext(value)傳入字符串'Slowest'。迭代器再次調(diào)用next(value)把'Slowest'傳回生成器函數(shù)中的第一個yield,yield取得這個字符串,立即恢復(fù)生成器執(zhí)行,把這個字符串賦值給變量r1。生成器函數(shù)中的代碼繼續(xù)執(zhí)行,到第二個yield處暫停,此時創(chuàng)建并返回第二個最終值為'slow'的Promise,但此時Promise是待定狀態(tài),200毫秒后才會解決。
繼續(xù),在迭代器代碼中,變量next再次拿到一個對象{ value: <Promise [pending]>, done: false}。再次進(jìn)入IIFE,傳入next。檢查next.done不等于false,在else塊中把next.value封裝到一個Promise.resolve(next.value)中……
看,下面又是一個:
function run(generator) {return new Promise((resolve, reject) => {const it = generator()step(() => it.next())function step(nextFn) {const result = runNext(nextFn)if (result.done) {resolve(result.value)return}Promise.resolve(result.value).then(value => step(() => it.next(value)), err => step(() => it.throw(err)))}function runNext(nextFn) {try {return nextFn()} catch (err) {reject(err)}}}) } 復(fù)制代碼6. 為什么說Async函數(shù)是語法糖
有了這個運行函數(shù),我們可以比較一下下面兩個example()函數(shù):
第一個example()是通過生成器運行程序控制異步代碼;第二個example()是一個異步(Async)函數(shù),通過async/await控制異步代碼。
它們的區(qū)別只在于前者多了一層run函數(shù)封裝,使用yield而不是await,而且沒有async關(guān)鍵字修飾。除此之外,核心代碼完全一樣!
現(xiàn)在,大家再看到類似下面的異步函數(shù),能想到什么?
async function example() {const r1 = await new Promise(resolve =>setTimeout(resolve, 500, 'slowest'))const r2 = await new Promise(resolve =>setTimeout(resolve, 200, 'slow'))return [r1, r2] }example().then(result => console.log(result)) // ['slowest', 'slow'] 復(fù)制代碼是的,Async函數(shù)或者說async/await就是基于Promise、Iterator和Generator構(gòu)造的一塊充滿苦澀和香甜、讓人回味無窮的“語法糖”!記住,Async function = Promise + Iterator + Generator,或者“Async函數(shù)原來是PIG”。
7. 參考資料
- ECMAScript 2018
- Practical Modern JavaScript
- You Don't Know JS: Async & Performance
- Understanding ECMAScript 6
- Exploring ES6
總結(jié)
以上是生活随笔為你收集整理的小哥哥小姐姐,来尝尝 Async 函数这块语法糖的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JAVA系列之JVM优化
- 下一篇: 软件开发十三种文档格式