這篇文章是講JS異步原理和實現方式的第四篇文章,前面三篇是:javascript
setTimeout和setImmediate到底誰先執行,本文讓你完全理解Event Loopjava
從發佈訂閱模式入手讀懂Node.js的EventEmitter源碼node
手寫一個Promise/A+,完美經過官方872個測試用例git
本文主要會講Generator的運用和實現原理,而後咱們會去讀一下co模塊的源碼,最後還會提一下async/await。github
本文所有例子都在GitHub上:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/JavaScript/Generator編程
異步編程一直是JS的核心之一,業界也是一直在探索不一樣的解決方法,從「回調地獄」到發佈訂閱模式,再到Promise,都是在優化異步編程。儘管Promise已經很優秀了,也不會陷入「回調地獄」,可是嵌套層數多了也會有一連串的then
,始終不能像同步代碼那樣直接往下寫就好了。Generator是ES6引入的進一步改善異步編程的方案,下面咱們先來看看基本用法。segmentfault
Generator的中文翻譯是「生成器」,其實他要乾的事情也是一個生成器,一個函數若是加了*
,他就會變成一個生成器函數,他的運行結果會返回一個迭代器對象,好比下面的代碼:promise
// gen是一個生成器函數 function* gen() { let a = yield 1; let b = yield a + 2; yield b + 3; } let itor = gen(); // 生成器函數運行後會返回一個迭代器對象,即itor。
ES6規範中規定迭代器必須有一個next
方法,這個方法會返回一個對象,這個對象具備done
和value
兩個屬性,done
表示當前迭代器內容是否已經執行完,執行完爲true
,不然爲false
,value
表示當前步驟返回的值。在generator
具體運用中,每次遇到yield
關鍵字都會暫停執行,當調用迭代器的next
時,會將yield
後面表達式的值做爲返回對象的value
,好比上面生成器的執行結果以下:網絡
咱們能夠看到第一次調next
返回的就是第一個yeild
後面表達式的值,也就是1。須要注意的是,整個迭代器目前暫停在了第一個yield
這裏,給變量a
賦值都沒執行,要調用下一個next
的時候纔會給變量a
賦值,而後一直執行到第二個yield
。那應該給a
賦什麼值呢?從代碼來看,a
的值應該是yield
語句的返回值,可是yield
自己是沒有返回值的,或者說返回值是undefined
,若是要給a
賦值須要下次調next
的時候手動傳進去,咱們這裏傳一個4,4就會做爲上次yield
的返回值賦給a
:app
能夠看到第二個yield
後面的表達式a + 2
的值是6,這是由於咱們傳進去的4被做爲上一個yield
的返回值了,而後計算a + 2
天然就是6了。
咱們繼續next
,把這個迭代器走完:
上圖是接着前面運行的,圖中第一個next
返回的value
是NaN
是由於咱們調next
的時候沒有傳參數,也就是說b
爲undefined
,undefined + 3
就爲NaN
了 。最後一個next
實際上是把函數體執行完了,這時候的value
應該是這個函數return
的值,可是由於咱們沒有寫return
,默認就是return undefined
了,執行完後done
會被置爲true
。
迭代器還有個方法是throw
,這個方法能夠在函數體外部拋出錯誤,而後在函數裏面捕獲,仍是上面那個例子:
function* gen() { let a = yield 1; let b = yield a + 2; yield b + 3; } let itor = gen();
咱們此次不用next
執行了,直接throw
錯誤出來:
這個錯誤由於咱們沒有捕獲,因此直接拋到最外層來了,咱們能夠在函數體裏面捕獲他,稍微改下:
function* gen() { try { let a = yield 1; let b = yield a + 2; yield b + 3; } catch (e) { console.log(e); } } let itor = gen();
而後再來throw
下:
這個圖能夠看出來,錯誤在函數裏裏面捕獲了,走到了catch
裏面,這裏面只有一個console
同步代碼,整個函數直接就運行結束了,因此done
變成true
了,固然catch
裏面能夠繼續寫yield
而後用next
來執行。
迭代器還有個return
方法,這個方法就很簡單了,他會直接終止當前迭代器,將done
置爲true
,這個方法的參數就是迭代器的value
,仍是上面的例子:
function* gen() { let a = yield 1; let b = yield a + 2; yield b + 3; } let itor = gen();
此次咱們直接調用return
:
簡單理解,yield*
就是在生成器裏面調用另外一個生成器,可是他並不會佔用一個next
,而是直接進入被調用的生成器去運行。
function* gen() { let a = yield 1; let b = yield a + 2; } function* gen2() { yield 10 + 5; yield* gen(); } let itor = gen2();
上面代碼咱們第一次調用next
,值天然是10 + 5
,即15,而後第二次調用next
,其實就走到了yield*
了,這其實就至關於調用了gen
,而後執行他的第一個yield
,值就是1。
其實Generator就是實現了協程,協程是一個比線程還小的概念。一個進程能夠有多個線程,一個線程能夠有多個協程,可是一個線程同時只能有一個協程在運行。這個意思就是說若是當前協程能夠執行,好比同步代碼,那就執行他,若是當前協程暫時不能繼續執行,好比他是一個異步讀文件的操做,那就將它掛起,而後去執行其餘協程,等這個協程結果回來了,能夠繼續了再來執行他。yield
其實就至關於將當前任務掛起了,下次調用再從這裏開始。協程這個概念其實不少年前就已經被提出來了,其餘不少語言也有本身的實現。Generator至關於JS實現的協程。
前面講了Generator的基本用法,咱們用它來處理一個異步事件看看。我仍是使用前面文章用到過的例子,三個網絡請求,請求3依賴請求2的結果,請求2依賴請求1的結果,若是使用回調是這樣的:
const request = require("request"); request('https://www.baidu.com', function (error, response) { if (!error && response.statusCode == 200) { console.log('get times 1'); request('https://www.baidu.com', function(error, response) { if (!error && response.statusCode == 200) { console.log('get times 2'); request('https://www.baidu.com', function(error, response) { if (!error && response.statusCode == 200) { console.log('get times 3'); } }) } }) } });
咱們此次使用Generator來解決「回調地獄」:
const request = require("request"); function* requestGen() { function sendRequest(url) { request(url, function (error, response) { if (!error && response.statusCode == 200) { console.log(response.body); // 注意這裏,引用了外部的迭代器itor itor.next(response.body); } }) } const url = 'https://www.baidu.com'; // 使用yield發起三個請求,每一個請求成功後再繼續調next const r1 = yield sendRequest(url); console.log('r1', r1); const r2 = yield sendRequest(url); console.log('r2', r2); const r3 = yield sendRequest(url); console.log('r3', r3); } const itor = requestGen(); // 手動調第一個next itor.next();
這個例子中咱們在生成器裏面寫了一個請求方法,這個方法會去發起網絡請求,每次網絡請求成功後又繼續調用next執行後面的yield
,最後是在外層手動調一個next
觸發這個流程。這其實就相似一個尾調用,這樣寫能夠達到效果,可是在requestGen
裏面引用了外面的迭代器itor
,耦合很高,並且很差複用。
爲了解決前面說的耦合高,很差複用的問題,就有了thunk函數。thunk函數理解起來有點繞,我先把代碼寫出來,而後再一步一步來分析它的執行順序:
function Thunk(fn) { return function(...args) { return function(callback) { return fn.call(this, ...args, callback) } } } function run(fn) { let gen = fn(); function next(err, data) { let result = gen.next(data); if(result.done) return; result.value(next); } next(); } // 使用thunk方法 const request = require("request"); const requestThunk = Thunk(request); function* requestGen() { const url = 'https://www.baidu.com'; let r1 = yield requestThunk(url); console.log(r1.body); let r2 = yield requestThunk(url); console.log(r2.body); let r3 = yield requestThunk(url); console.log(r3.body); } // 啓動運行 run(requestGen);
這段代碼裏面的Thunk函數返回了好幾層函數,咱們從他的使用入手一層一層剝開看:
requestThunk
是Thunk運行的返回值,也就是第一層返回值,參數是request
,也就是:
function(...args) { return function(callback) { return request.call(this, ...args, callback); // 注意這裏調用的是request } }
run
函數的參數是生成器,咱們看看他到底幹了啥:
- run裏面先調用生成器,拿到迭代器
gen
,而後自定義了一個next
方法,並調用這個next
方法,爲了便於區分,我這裏稱這個自定義的next
爲局部next
局部
next
會調用生成器的next
,生成器的next
其實就是yield requestThunk(url)
,參數是咱們傳進去的url
,這就調到咱們前面的那個方法,這個yield
返回的value
實際上是:function(callback) { return request.call(this, url, callback); }- 檢測迭代器是否已經迭代完畢,若是沒有,就繼續調用第二步的這個函數,這個函數其實才真正的去
request
,這時候傳進去的參數是局部next
,局部next
也做爲了request
的回調函數。- 這個回調函數在執行時又會調
gen.next
,這樣生成器就能夠繼續往下執行了,同時gen.next
的參數是回調函數的data
,這樣,生成器裏面的r1
其實就拿到了請求的返回值。
Thunk函數就是這樣一種能夠自動執行Generator的函數,由於Thunk函數的包裝,咱們在Generator裏面能夠像同步代碼那樣直接拿到yield
異步代碼的返回值。
co模塊是一個很受歡迎的模塊,他也能夠自動執行Generator,他的yield後面支持thunk和Promise,咱們先來看看他的基本使用,而後再去分析下他的源碼。
官方GitHub:https://github.com/tj/co
前面咱們講了thunk函數,咱們仍是從thunk函數開始。代碼仍是用咱們前面寫的thunk函數,可是由於co支持的thunk是隻接收回調函數的函數形式,咱們使用時須要調整下:
// 仍是以前的thunk函數 function Thunk(fn) { return function(...args) { return function(callback) { return fn.call(this, ...args, callback) } } } // 將咱們須要的request轉換成thunk const request = require('request'); const requestThunk = Thunk(request); // 轉換後的requestThunk其實能夠直接用了 // 用法就是 requestThunk(url)(callback) // 可是咱們co接收的thunk是 fn(callback)形式 // 咱們轉換一下 // 這時候的baiduRequest也是一個函數,url已經傳好了,他只須要一個回調函數作參數就行 // 使用就是這樣:baiduRequest(callback) const baiduRequest = requestThunk('https://www.baidu.com'); // 引入co執行, co的參數是一個Generator // co的返回值是一個Promise,咱們能夠用then拿到他的結果 const co = require('co'); co(function* () { const r1 = yield baiduRequest; const r2 = yield baiduRequest; const r3 = yield baiduRequest; return { r1, r2, r3, } }).then((res) => { // then裏面就能夠直接拿到前面返回的{r1, r2, r3} console.log(res); });
其實co官方是建議yield後面跟Promise的,雖然支持thunk,可是將來可能會移除。使用Promise,咱們代碼寫起來其實更簡單,直接用fetch就行,不用包裝Thunk。
const fetch = require('node-fetch'); const co = require('co'); co(function* () { // 直接用fetch,簡單多了,fetch返回的就是Promise const r1 = yield fetch('https://www.baidu.com'); const r2 = yield fetch('https://www.baidu.com'); const r3 = yield fetch('https://www.baidu.com'); return { r1, r2, r3, } }).then((res) => { // 這裏一樣能夠拿到{r1, r2, r3} console.log(res); });
本文的源碼分析基於co模塊4.6.0版本,源碼:https://github.com/tj/co/blob/master/index.js
仔細看源碼會發現他代碼並很少,總共兩百多行,一半都是在進行yield後面的參數檢測和處理,檢測他是否是Promise,若是不是就轉換爲Promise,因此即便你yield後面傳的thunk,他仍是會轉換成Promise處理。轉換Promise的代碼相對比較獨立和簡單,我這裏不詳細展開了,這裏主要仍是講一講核心方法co(gen)
。下面是我複製的去掉了註釋的簡化代碼:
function co(gen) { var ctx = this; var args = slice.call(arguments, 1); return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); return null; } function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); }
gen
手動調用一次onFulfilled
,開啓迭代
onFulfilled
接收一個參數res
,第一次調用是沒有傳這個參數,這個參數主要是用來接收後面的then返回的結果。- 而後調用
gen.next
,注意這個的返回值ret的形式是{value, done},而後將這個ret傳給局部的next
而後執行局部next,他接收的參數是yield返回值{value, done}
- 這裏先檢測迭代是否完成,若是完成了,就直接將整個promise resolve。
- 這裏的value是yield後面表達式的值,多是thunk,也多是promise
- 將value轉換成promise
- 將轉換後的promise拿出來執行,成功的回調是前面的
onFulfilled
onFulfilled
,這是第二次執行onFulfilled
了。此次執行的時候傳入的參數res是上次異步promise的執行結果,對應咱們的fetch就是拿回來的數據,這個數據傳給第二個gen.next
,效果就是咱們代碼裏面的賦值給了第一個yield
前面的變量r1
。而後繼續局部next,這個next其實就是執行第二個異步Promise了。這個promise的成功回調又繼續調用gen.next
,這樣就不斷的執行下去,直到done
變成true
爲止。onRejected
方法,這個方法其實做爲了異步promise的錯誤分支,這個函數裏面直接調用了gen.throw
,這樣咱們在Generator裏面能夠直接用try...catch...
拿到錯誤。須要注意的是gen.throw
後面還繼續調用了next(ret)
,這是由於在Generator的catch
分支裏面還可能繼續有yield
,好比錯誤上報的網絡請求,這時候的迭代器並不必定結束了。最後提一下async/await
,先來看一下用法:
const fetch = require('node-fetch'); async function sendRequest () { const r1 = await fetch('https://www.baidu.com'); const r2 = await fetch('https://www.baidu.com'); const r3 = await fetch('https://www.baidu.com'); return { r1, r2, r3, } } // 注意async返回的也是一個promise sendRequest().then((res) => { console.log('res', res); });
咋一看這個跟前面promise版的co是否是很像,返回值都是一個promise,只是Generator換成了一個async
函數,函數裏面的yield
換成了await
,並且外層不須要co來包裹也能夠自動執行了。其實async函數就是Generator加自動執行器的語法糖,能夠理解爲從語言層面支持了Generator的自動執行。上面這段代碼跟co版的promise其實就是等價的。
next
才能一條一條執行yield
next
的返回值是{value, done},value
是yield後面表達式的值yield
語句自己並無返回值,下次調next
的參數會做爲上一個yield
語句的返回值thunk
的時候提供了一種方案,co
模塊也是一個很受歡迎的自動執行方案gen.next
,同時這個方法自己又會傳到回調函數或者promise的成功分支裏面,異步結束後又繼續調用這個局部方法,這個局部方法又調用gen.next
,這樣一直迭代,直到迭代器執行完畢。async/await
實際上是Generator和自動執行器的語法糖,寫法和實現原理都相似co模塊的promise模式。文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges