編者注:衆所周知,JS 最大的特性就是異步,異步提升了性能可是卻給咱們編寫帶來了必定困難,造就了使人髮指的回調地獄。爲了解決這個問題,一個又一個的解決方案被提出來。今天咱們請來了 《JavaScript 高級程序設計》等多本書的知名譯者 @李鬆峯 老師給咱們講解下各類異步函數編寫的解決方案以及各類內涵。javascript
本次內容是基於以前分享的文字版,若想看重點的話能夠看以前的 PPT:ppt.baomitu.com/d/fd045abb
也能夠查看以前的分享視頻:cloud.live.360vcloud.net/theater/pla…前端
ES7(ECMAScript 2016)推出了Async函數(async/await
),實現了以順序、同步代碼的編寫方式來控制異步流程,完全解決了困擾JavaScript開發者的「回調地獄」問題。好比,以前須要嵌套回調的異步邏輯:java
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)
})
})
複製代碼
如今能夠寫成以下同步代碼的樣式了:react
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']
複製代碼
Async函數須要在function
前面添加async
關鍵字,同時內部以await
關鍵字來「阻塞」異步操做,直到異步操做返回結果,而後再繼續執行。在沒有Async函數之前,咱們沒法想象下面的異步代碼能夠直接拿到結果:程序員
const r1 = ajax('url')
console.log(r1)
// undefined
複製代碼
這固然是不可能的,異步函數的結果只能在回調裏拿到。能夠說,Async函數是JavaScript程序員在探索如何高效異步編程過程當中踩「坑」以後的努力「自救」得到的成果——不是「糖果」。然而,讀者小哥哥小姐姐可能有所不知,Async函數其實是一個語法糖(果真是「糖果」嗎?),它的背後是ES6(ECMAScript 2015)中推出的Promise、Iterator和Generator,咱們簡稱「PIG」。本文就帶各位好好品嚐品嚐這塊語法糖,感覺一個PIG是如何成就Async函數的。ajax
當前JavaScript編程主要是異步編程。爲何這麼說呢?網頁或Web開發最先從2005年Ajax流行開始,逐步向重交互時代邁進。特別是SPA(Single Page Application,單頁應用)流行以後,一度有人提出「Web頁面要轉向Web應用,並且要媲美原生應用」。現在在前端開發組件化的背景下催生的Angular、React和Vue,都是SPA進一步演化的結果。typescript
Web應用或開發重交互的特徵愈來愈明顯,意味着什麼?意味着按照瀏覽器這個運行時的特性,頁面在首次加載過程當中,與JavaScript相關的主要任務就是加載基礎運行庫和擴展庫(包括給低版本瀏覽器打補丁的腳本),而後初始化和設置頁面的狀態。首次加載以後,用戶對頁面的操做、數據I/O以及DOM更新,就所有交由異步JavaScript腳本管理。因此,目前JavaScript編程最大的應用是Web交互,而Web交互的核心就是異步邏輯。編程
然而,ES6以前JavaScript中控制異步流程的手段只有事件和回調。好比下面的示例展現了經過原生XMLHttpRequest
對象發送異步請求,而後給onload
和onerror
事件分別註冊成功和錯誤處理函數:數組
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();
複製代碼
下面的代碼展現了Node.js經典的「先傳錯誤」的回調。但這裏要重點提一下,這種函數式編程風格也叫CPS,即Continuation Passing Style,我翻譯成「後續操做傳遞風格」。由於調用readFile
傳入了表示後續操做的一個回調函數。這一塊就不展開了。promise
// Node.js
fs.readFile('file.txt', function (error, data) {
if (error) {
// ...
}
console.log(data);
}
);
複製代碼
事件和回調有不少問題,主要是它們只適用於簡單的狀況。邏輯一複雜,代碼的編寫和維護成本就成倍上升。好比,你們熟知的「回調地獄」。更重要的是,回調模式的異步本質與人類同步、順序的思惟模式是相悖的。
爲了應對愈來愈複雜的異步編程需求,ES6推出瞭解決上述問題的Promise。
Promise,人們廣泛的理解就是:「Promise是一個將來值的佔位符」。也就是說,從語義上講,一個Promise對象表明一個對將來值的「承諾」(promise),這個承諾未來若是「兌現」(fulfill),就會「解決」(resolve)爲一個有意義的數據;若是「拒絕」(reject),就會「解決」爲一個「拒絕理由」(rejection reason),就是一個錯誤消息。
Promise對象的狀態很簡單,一輩子下來的狀態是pending
(待定),未來兌現了,狀態變成fulfilled
;拒絕了,狀態變成rejected
。fulfilled
和rejected
顯然是一種「肯定」(settled)狀態。以上狀態轉換是不可逆的,因此Promise很單純,好控制,哈哈。
如下是Promise相關的全部API。前3個是建立Promise對象的(稍後有例子),後4箇中的前2個是用於註冊反應函數的(稍後有例子),後2個是用於控制併發和搶佔的:
如下是經過Prmoise(executor)
構造函數建立Promise實例的詳細過程:要傳入一個「執行函數」(executor),這個執行函數又接收兩個參數「解決函數」(resolver)和「拒絕函數」(rejector),代碼中分別對應變量resolve
和reject
,做用分別是將新建對象的狀態由pending
改成fulfilled
和rejected
,同時返回「兌現值」(fulfillment)和「拒絕理由」(rejection)。固然,resolve
和reject
都是在異步操做的回調中調用的。調用以後,運行時環境(瀏覽器引擎或Node.js的libuv)中的事件循環調度機制會把與之相關的反應函數——兌現反應函數或拒絕反應函數以及相關的參數添加到「微任務」隊列,以便下一次「循檢」(tick)時調度到JavaScript線程去執行。
如前所述,Promise對象的狀態由pending
變成fulfilled
,就會執行「兌現反應函數」(fulfillment reaction);而變成rejected
,就會執行「拒絕反應函數」(rejection reaction)。以下例所示,常規的方式是經過p.then()
註冊兌現函數,經過p.catch()
註冊拒絕函數:
p.then(res => { // 兌現反應函數
// res === 'random success'
})
p.catch(err => { // 拒絕反應函數
// err === 'random failure'
})
複製代碼
固然還有很是規的方式,並且有時候很是規方式可能更好用:
// 經過一個.then()方法同時註冊兌現和拒絕函數
p.then(
res => {
// handle response
},
err => {
// handle error
}
)
// 經過.then()方法只註冊一個函數:兌現函數
p.then(res => {
// handle response
})
// 經過.then()方法只傳入拒絕函數,兌現函數的位置傳null
p.then(null, err => {
// handle error
})
複製代碼
關於Promise就這樣吧。ES6除了Promise,還推出了Iterator(迭代器)和Generator(生成器),因而就有成就Async函數的PIG組合。下面咱們分別簡單看一看Iterator和Generator。
要理解Iterator或者迭代器,最簡單的方式是看它的接口:
interface IteratorResult {
done: boolean;
value: any;
}
interface Iterator {
next(): IteratorResult;
}
interface Iterable {
[Symbol.iterator](): Iterator
}
複製代碼
先從中間的Iterator
看。
什麼是迭代器?它是一個對象,有一個next()
方法,每次調用next()
方法,就會返回一個迭代器結果(看第一個接口IteratorResult
)。而這個迭代器結果,一樣仍是一個對象,這個對象有兩個屬性:done
和value
,其中done
是一個布爾值,false
表示迭代器迭代的序列沒有結束;true
表示迭代器迭代的序列結束了。而value
就是迭代器每次迭代真正返回的值。
再看最後一個接口Iterable
,翻譯成「可迭代對象」,它有一個[Symbol.iterator]()
方法,這個方法會返回一個迭代器。
能夠結合前面的接口定義和下面這張圖來理解可迭代對象(實現了「可迭代協議」)、迭代器(實現了「迭代器協議」)和迭代器結果這3個簡單而又重要的概念(暫時理解不了也不要緊,後面還有一個無窮序列的例子,能夠幫助你們理解)。
可迭代對象是一個咱們很是熟悉的概念,數組、字符串以及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']
複製代碼
以上示例分別使用for...of
、擴展操做符(...
)和Array.from()
方法來迭代了前面定義的sequence
這個可迭代對象。
下面再看一個經過迭代器建立無窮序列的小例子,經過這個例子咱們再來深刻理解與迭代器相關的概念。
const random = {
[Symbol.iterator]: () => ({
next: () => ({ value: Math.random() })
})
}
// 運行這行代碼會怎麼樣?
[...random]
// 這行呢?
Array.from(random)
複製代碼
這個例子使用兩個ES6的箭頭函數定義了兩個方法,建立了三個對象。
最內層的對象{ value: Math.random() }
很明顯是一個「迭代器結果」(IteratorResult
)對象,由於它有一個value
屬性和一個……,等等,done
屬性呢?這裏沒有定義done
屬性,因此每次迭代(調用next()
)時訪問IteratorResult.done
都會返回false
;因此這個迭代器結果的定義至關於{ value: Math.random() , done: false }
。顯然,done
永遠不多是true
,因此這是一個無窮隨機數序列!
interface IteratorResult {
done: boolean;
value: any;
}
複製代碼
再往外看,返回這個迭代器結果對象的箭頭函數被賦值給了外層對象的next()
方法。根據Iterator
接口的定義,若是一個對象包含一個next()
方法,而這個方法的返回值又是一個迭代器結果,那麼這個對象是什麼?沒錯,就是迭代器。好,第二個對象是一個迭代器!
interface Iterator {
next(): IteratorResult;
}
複製代碼
再往外看,返回這個迭代器對象的箭頭函數被賦值給了外層對象的[Symbol.iterator]()
方法。根據Iterable
接口的定義,若是一個對象包含一個[Symbol.iterator]()
方法,而這個方法的返回值又是一個迭代器,那麼這個對象是什麼?沒錯,就是可迭代對象。
interface Iterable {
[Symbol.iterator](): Iterator
}
複製代碼
好,到如今咱們應該完全理解迭代器及其相關概念了。下面繼續看例子。前面的例子定義了一個可迭代對象random
,這個對象的迭代器能夠無限返回隨機數,因此:
// 運行這行代碼會怎麼樣?
[...random]
// 這行呢?
Array.from(random)
複製代碼
是的,這兩行代碼都會致使程序(或運行時)崩潰!由於迭代器會不停地運行,阻塞JavaScript執行線程,最終可能因佔滿可用內存致使運行時中止響應,甚至崩潰。
那麼訪問無窮序列的正確方式是什麼?答案是使用解構賦值或給for...of
循環設置退出條件:
const [one, another] = random // 解析賦值,取得前兩個隨機數
console.log(one)
// 0.23235511826351285
console.log(another)
// 0.28749457537196577
for (const value of random) {
if (value > 0.8) { // 退出條件,隨機數大於0.8則中斷循環
break
}
console.log(value)
}
複製代碼
固然,使用無窮序列還有更高級的方式,鑑於本文的目的,在此就很少介紹了。下面咱們再說最後一個ES6的特性Generator。
依例,上接口:
interface Generator extends Iterator {
next(value?: any): IteratorResult;
[Symbol.iterator](): Iterator;
throw(exception: any);
}
複製代碼
能看來出生成器是什麼嗎?僅從它的接口來看,它既是一個迭代器,又是一個可迭代對象。沒錯,生成器所以又是迭代器的「增強版」,爲何?由於生成器還提供了一個關鍵字yield
,它返回的序列值會自動包裝在一個IteratorResult
(迭代器結果)對象中,省去了咱們手工編寫相應代碼的麻煩。下面就是一個生成器函數的定義:
function *gen() {
yield 'a'
yield 'b'
return 'c'
}
複製代碼
哎,接口定義的生成器不是一個對象嗎,怎麼是一個函數啊?
實際上,說生成器是對象或是函數都不確切。但咱們知道,調用生成器函數會返回一個迭代器(接口描述的就是這個對象),這個迭代器能夠控制返回它的生成器函數封裝的邏輯和數據。從這個意義上說,生成器由生成器函數及其返回的迭代器兩部分組成。再換句話說,生成器是一個籠統的概念,是一個統稱。(別急,一會你就明白這樣理解生成器的意義何在了。)
本節剛開始說了,生成器(返回的對象)「既是一個迭代器,又是一個可迭代對象」。下面咱們就來驗證一下:
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']
複製代碼
經過代碼中的註釋咱們獲得了所有答案。這裏有個小問題:「爲何迭代這個生成器返回的序列值中不包含字符'c'
呢?」
緣由在於,yield
返回的迭代器結果對象的done
屬性值都爲false
,因此'a'
和'b'
都是有效的序列值;而return
返回的雖然也是迭代器結果對象,但done
屬性的值倒是true
,true
表示序列結束,因此'c'
不會包含在迭代結果中。(若是沒有return
語句,代碼執行到生成器函數末尾,會隱式返回{ value: undefined, done: true}
。相信這一點不說你也知道。)
以上只是生成器做爲「增強版」迭代器的一面。接下來,咱們要接觸生成器真正強大的另外一面了!
生成器真正強大的地方,也是它有別於迭代器的地方,在於它不只能在每次迭代返回值,並且還能接收值。(固然,生成器的概念裏自己就有生成器函數嘛!函數固然能夠接收參數嘍。)等等,可不只僅是能夠給生成器函數傳參,而是還能夠給yield
表達式傳參!
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}
複製代碼
在上面這個簡單的生成器的例子中。咱們定義了一個生成器函數*gen()
,它接收一個參數x
。函數體內只有一個yield
表達式,好像啥也沒幹。可是,yield
表達式彷佛是一個「值的佔位符」,由於代碼在某個時刻會計算變量x
與這個「值」的乘積,並把該乘積賦值給變量y
。最後,函數返回y
。
這有點費解,下面咱們一步一步分析。
gen(6)
建立生成器的迭代器it
(前面說了,生成器包含迭代器及返回它的生成器函數),傳入數值6。it.next()
啓動生成器。此時生成器函數的代碼執行到第一個yield
表達式處暫停,並返回undefined
。(yield
並沒閒着,它看後面沒有顯式要返回的值,就只能返回默認的undefined
。)it.next(7)
恢復生成器執行。此時yield
接收到傳入的數值7,當即恢復生成器函數代碼的執行,並把本身替換成數值7。6 * 7
,獲得42,並把42賦給變量y
,最後返回y
。{value: 42, done: true}
。這個例子中只有一個yield
,假如還有更多的yield
,則第4步會到第二個yield
處再次暫停生成器函數的執行,返回一個值,以後重複第三、4步,即還能夠經過再調用it.next()
向生成器函數中傳入值。
咱們簡單總結一下,每次調用it.next()
,可能有下列4種狀況致使生成器暫停或中止執行:
yield
表達式返回序列中下一個值return
語句返回生成器函數的值({ done: true }
)throw
語句徹底中止生成器執行(後面會詳細解釋){ value: undefined, done: true}
注意 這裏的
return
和throw
既能夠在生成器函數內部調用,也能夠在生成器函數外部經過生成器的迭代器調用,好比:it.return(0)
、it.throw(new Error('Oops'))
。後面咱們會給出相應的例子。
由此,咱們瞭解到,生成器的獨到之處就在於它的yield
關鍵字。這個yield
有兩大神奇之處:1、它是生成器函數暫停和恢復執行的分界點;2、它是向外和向內傳值(包括錯誤/異常)的媒介。
提到錯誤/異常,下面咱們就來重點看一看生成器如何處理異常。畢竟,錯誤處理是使用回調方式編寫異步代碼的時候最讓JavaScript程序員頭疼的地方之一。
首先,咱們看「由內而外」的錯誤傳遞,即從生成器函數內部把錯誤拋到迭代器代碼中。
function *main() {
const x = yield "Hello World";
yield x.toLowerCase(); // 致使異常!
}
const it = main();
it.next().value; // Hello World
try {
it.next( 42 );
} catch (err) {
console.error(err); // TypeError
}
複製代碼
如代碼註釋所提示的,生成器函數的第二行代碼會致使異常(至於爲何,讀者能夠本身「人肉」執行代碼,推演一下)。因爲生成器函數內部沒有作異常處理,所以錯誤被拋給了生成器的迭代代碼,也就是it.next(42)
這行代碼。好在這行代碼被一個try/catch
包着,錯誤能夠正常捕獲並處理。
接下來,再看「由外而內」(準確地說,應該是「由外而內再而外」)的錯誤傳遞。
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
}
複製代碼
如代碼所示,迭代代碼經過it.throw('Oops')
拋出異常。這個異常是拋到生成器函數內的(經過迭代器it
)。拋進去以後,yield
表達式發現本身收到一個「燙手的山芋」,看看周圍也沒有異常處理邏輯「護駕」,因而眼疾手快,迅速又把這個異常給拋了出來。迭代器it
顯然是有準備的,它本意也是想先看看生成器函數內部有沒有邏輯負責異常處理(看註釋「 // *main()
會處理嗎?」),「沒有!」,它本身的try/catch
早已等候多時了。
前面咱們看到的對生成器的迭代傳值,包括傳遞錯誤,都是同步的。實際上,生成器的yield
表達式真正(哦,又一個「真正」)強大的地方在於:它在暫停生成器代碼執行之後,不是必須等待迭代器代碼同步調用it.next()
方法給它返回值,而是可讓迭代器在一個異步操做的回調中取得返回值,而後再經過it.next(res)
把值傳給它。
明白了嗎?yield
能夠等待一個異步操做的結果。從而讓本文開始提到的這種看似不可能的狀況變成可能:
const r1 = ajax('url')
console.log(r1)
// undefined
複製代碼
怎麼變呢,在異步操做前加個yield
呀:
const r1 = yield ajax('url')
console.log(r1)
// 此次r1就是真正的響應結果了
複製代碼
咱們仍是以一個返回Promise的異步操做爲例來講明這一點比較好。由於基於回調的異步操做,很容易能夠轉換成基於Promise的異步操做(好比jQuery的$.ajax()
或經過util.promisify
把Node.js中的異步方法轉換成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);
}
);
複製代碼
函數foo(x, y)
封裝了一個異步request
請求,返回一個Promise。調用foo(11, 31)
傳入參數後,request
就向拼接好的URL發送請求,返回待定(pending
)狀態的Promise對象。請求成功,則執行then()
中註冊的兌現反應函數,處理響應;請求失敗,則執行拒絕反應函數,處理錯誤。
接下來咱們要作的,就是將上面的代碼與生成器結合,讓生成器只關注發送請求和取得響應結果,而把異步操做的等待和回調處理邏輯做爲實現細節抽象出來。(「做爲細節」,對,咱們的目標是隻關注請求和結果,過程嘛,都是細節,哈哈~。)
function foo(x, y) {
return request(
"http://some.url.1/?x=" + x + "&y=" + y
);
}
function *main() {
try {
const result = yield foo(11, 31); // 異步函數調用!
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()`並恢復其執行
},
function(err){
it.throw(err); // 把`err`拋到`*main()`
}
);
複製代碼
注意,生成器函數(*main
)的yield
表達式中出現了異步函數調用:foo(11, 31)
。而咱們就要作的,就是在迭代器代碼中經過it.next()
拿到這個異步函數調用返回的Promise,而後正確地處理它。怎麼處理?咱們看代碼。
建立生成器的迭代器以後,const p = it.next().value;
返回了Promise p
。在p
的兌現反應函數中,咱們把拿到的響應res
經過it.next(res)
調用傳回了生成器函數中的yield
。yield
拿到響應結果res
以後,當即恢復生成器代碼的執行,把res
賦值給變量result
。因而,咱們成功地在生成器函數中,以同步代碼的書寫方式取得了異步請求的響應結果!神奇不?
(固然,若是異步請求發生錯誤,在p
的拒絕反應函數中也會經過it.throw(err)
把錯誤拋給生成器函數。但這個如今不重要。)
好啦,目標達成:咱們利用生成器的同步代碼,實現了對異步操做的完美控制。然而,還有一個問題。上面例子中的生成器只包裝了一個異步操做,若是是多個異步操做怎麼辦呢?這時候,最好有一段通用的用於處理生成器函數的代碼,不管其中包含多少異步操做,這段代碼都能自動完成對Promise的接收、等待和響應/錯誤傳遞等這些「細節」工做。
那不就是一個基於Promise的生成器運行程序嗎?
綜前所述,咱們想要的是這樣一個結果:
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']
複製代碼
即定義一個通用的運行函數run
,它負責處理傳給它的生成器函數中包裝的任意多個異步操做。針對每一個操做,它都會正確地返回異步結果,或者向生成器函數中拋出異常。而運行這個函數的最終結果,也是返回一個Promise,這個Promise包含生成器函數返回的全部異步操做的結果(上例)。
已經有聰明人實現了這樣的運行程序,下面咱們就給出兩個實現,你們能夠本身嘗試去運行一下,而後「人肉」執行,加深理解。
注意 在ES7推出Async函數以前,飽受回調之苦的JavaScript程序員就是靠相似的運行程序結合生成器給本身「續命」的。事實上,在ES6以前(沒有Promise、沒有生成器)的「蠻荒時代」,不屈不撓又神機妙算的JavaScript程序員們就已經摸索出/找到了Thenable(Promise的前身)和相似生成器的實現方法(好比regenerator),讓瀏覽器能支持本身以同步風格編寫異步代碼的高效幹活兒夢。
苦哉!偉哉!悲夫,絞兮乎!
這是一個:
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)
}
複製代碼
供參考的「人肉」執行過程
(調用
run
的代碼見本節開頭。)這個
run
函數接收一個生成器函數做爲參數,而後當即建立了生成器的迭代器it
(看上面run
函數的代碼)。而後,它返回一個Promise,是經過
Promise.resolve()
直接建立的。咱們給這個Promise的
.then()
方法傳入了一個兌現反應函數(這個函數必定會被調用,由於Promise是兌現的),名叫handleNext(value)
,它接收一個參數value
。第一次調用時,不會傳入任何值,所以value
的值是undefined
。接下來,第一次調用
it.next(value)
啓動生成器,傳入undefined
。生成器的第一個yield
會返回一個待定狀態的Promise,至少500ms以後纔會解決。此時變量
next
的值是{ value: < Promise [pending]>, done: false}
。接着,把
next
傳給下面的IIFE(Immediately Invoked Function Expression,當即調用函數表達式),這個函數叫handleResult
(處理結果)。在
handleResult(next)
內部,首先檢查next.done
,不等於true
,進入else
子句。此時經過Promise.resolve(next.value)
包裝next.value
:等待返回的Promise解決,解決以後拿到字符串值'Slowest'
,而後傳給兌現反應函數handleNext(value)
。至此,第一個異步操做的前半程處理完畢。接着,再次調用
handleNext(value)
傳入字符串'Slowest'
。迭代器再次調用next(value)
把'Slowest'
傳回生成器函數中的第一個yield
,yield
取得這個字符串,當即恢復生成器執行,把這個字符串賦值給變量r1
。生成器函數中的代碼繼續執行,到第二個yield處
暫停,此時建立並返回第二個最終值爲'slow'
的Promise,但此時Promise是待定狀態,200毫秒後纔會解決。繼續,在迭代器代碼中,變量
next
再次拿到一個對象{ value: <Promise [pending]>, done: false}
。再次進入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)
}
}
})
}
複製代碼
有了這個運行函數,咱們能夠比較一下下面兩個example()
函數:
第一個example()
是經過生成器運行程序控制異步代碼;第二個example()
是一個異步(Async)函數,經過async/await
控制異步代碼。
它們的區別只在於前者多了一層run
函數封裝,使用yield
而不是await
,並且沒有async
關鍵字修飾。除此以外,核心代碼徹底同樣!
如今,你們再看到相似下面的異步函數,能想到什麼?
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']
複製代碼
是的,Async函數或者說async/await
就是基於Promise、Iterator和Generator構造的一塊充滿苦澀和香甜、讓人耐人尋味的「語法糖」!記住,Async function = Promise + Iterator + Generator,或者「Async函數原來是PIG」。