var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
爲了得到最終的執行結果,你須要這樣作:node
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
首先執行 Generator 函數,獲取遍歷器對象。git
而後使用 next 方法,執行異步任務的第一階段,即 fetch(url)。github
注意,因爲 fetch(url) 會返回一個 Promise 對象,因此 result 的值爲:json
{ value: Promise { <pending> }, done: false }
最後咱們爲這個 Promise 對象添加一個 then 方法,先將其返回的數據格式化(data.json()
),再調用 g.next,將得到的數據傳進去,由此能夠執行異步任務的第二階段,代碼執行完畢。api
上節咱們只調用了一個接口,那若是咱們調用了多個接口,使用了多個 yield,咱們豈不是要在 then 函數中不斷的嵌套下去……promise
因此咱們來看看執行多個異步任務的狀況:異步
var fetch = require('node-fetch'); function* gen() { var r1 = yield fetch('https://api.github.com/users/github'); var r2 = yield fetch('https://api.github.com/users/github/followers'); var r3 = yield fetch('https://api.github.com/users/github/repos'); console.log([r1.bio, r2[0].login, r3[0].full_name].join('\n')); }
爲了得到最終的執行結果,你可能要寫成:函數
var g = gen(); var result1 = g.next(); result1.value.then(function(data){ return data.json(); }) .then(function(data){ return g.next(data).value; }) .then(function(data){ return data.json(); }) .then(function(data){ return g.next(data).value }) .then(function(data){ return data.json(); }) .then(function(data){ g.next(data) });
但我知道你確定不想寫成這樣……fetch
其實,利用遞歸,咱們能夠這樣寫:優化
function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value.then(function(data) { return data.json(); }).then(function(data) { next(data); }); } next(); } run(gen);
其中的關鍵就是 yield 的時候返回一個 Promise 對象,給這個 Promise 對象添加 then 方法,當異步操做成功時執行 then 中的 onFullfilled 函數,onFullfilled 函數中又去執行 g.next,從而讓 Generator 繼續執行,而後再返回一個 Promise,再在成功時執行 g.next,而後再返回……
在 run 這個啓動器函數中,咱們在 then 函數中將數據格式化 data.json()
,但在更普遍的狀況下,好比 yield 直接跟一個 Promise,而非一個 fetch 函數返回的 Promise,由於沒有 json 方法,代碼就會報錯。因此爲了更具有通用性,連同這個例子和啓動器,咱們修改成:
var fetch = require('node-fetch'); function* gen() { var r1 = yield fetch('https://api.github.com/users/github'); var json1 = yield r1.json(); var r2 = yield fetch('https://api.github.com/users/github/followers'); var json2 = yield r2.json(); var r3 = yield fetch('https://api.github.com/users/github/repos'); var json3 = yield r3.json(); console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n')); } function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value.then(function(data) { next(data); }); } next(); } run(gen);
只要 yield 後跟着一個 Promise 對象,咱們就能夠利用這個 run 函數將 Generator 函數自動執行。
yield 後必定要跟着一個 Promise 對象才能保證 Generator 的自動執行嗎?若是隻是一個回調函數呢?咱們來看個例子:
首先咱們來模擬一個普通的異步請求:
function fetchData(url, cb) { setTimeout(function(){ cb({status: 200, data: url}) }, 1000) }
咱們將這種函數改形成:
function fetchData(url) { return function(cb){ setTimeout(function(){ cb({status: 200, data: url}) }, 1000) } }
對於這樣的 Generator 函數:
function* gen() { var r1 = yield fetchData('https://api.github.com/users/github'); var r2 = yield fetchData('https://api.github.com/users/github/followers'); console.log([r1.data, r2.data].join('\n')); }
若是要得到最終的結果:
var g = gen(); var r1 = g.next(); r1.value(function(data) { var r2 = g.next(data); r2.value(function(data) { g.next(data); }); });
若是寫成這樣的話,咱們會面臨跟第一節一樣的問題,那就是當使用多個 yield 時,代碼會循環嵌套起來……
一樣利用遞歸,因此咱們能夠將其改造爲:
function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value(next); } next(); } run(gen);
由此能夠看到 Generator 函數的自動執行須要一種機制,即當異步操做有告終果,可以自動交回執行權。
而兩種方法能夠作到這一點。
(1)回調函數。將異步操做進行包裝,暴露出回調函數,在回調函數裏面交回執行權。
(2)Promise 對象。將異步操做包裝成 Promise 對象,用 then 方法交回執行權。
在兩種方法中,咱們各寫了一個 run 啓動器函數,那咱們能不能將這兩種方式結合在一些,寫一個通用的 run 函數呢?咱們嘗試一下:
// 初版 function run(gen) { var gen = gen(); function next(data) { var result = gen.next(data); if (result.done) return; if (isPromise(result.value)) { result.value.then(function(data) { next(data); }); } else { result.value(next) } } next() } function isPromise(obj) { return 'function' == typeof obj.then; } module.exports = run;
其實實現的很簡單,判斷 result.value 是不是 Promise,是就添加 then 函數,不是就直接執行。
咱們已經寫了一個不錯的啓動器函數,支持 yield 後跟回調函數或者 Promise 對象。
如今有一個問題須要思考,就是咱們如何得到 Generator 函數的返回值呢?又若是 Generator 函數中出現了錯誤,就好比 fetch 了一個不存在的接口,這個錯誤該如何捕獲呢?
這很容易讓人想到 Promise,若是這個啓動器函數返回一個 Promise,咱們就能夠給這個 Promise 對象添加 then 函數,當全部的異步操做執行成功後,咱們執行 onFullfilled 函數,若是有任何失敗,就執行 onRejected 函數。
咱們寫一版:
// 第二版 function run(gen) { var gen = gen(); return new Promise(function(resolve, reject) { function next(data) { try { var result = gen.next(data); } catch (e) { return reject(e); } if (result.done) { return resolve(result.value) }; var value = toPromise(result.value); value.then(function(data) { next(data); }, function(e) { reject(e) }); } next() }) } function isPromise(obj) { return 'function' == typeof obj.then; } function toPromise(obj) { if (isPromise(obj)) return obj; if ('function' == typeof obj) return thunkToPromise(obj); return obj; } function thunkToPromise(fn) { return new Promise(function(resolve, reject) { fn(function(err, res) { if (err) return reject(err); resolve(res); }); }); } module.exports = run;
與初版有很大的不一樣:
首先,咱們返回了一個 Promise,當 result.done
爲 true 的時候,咱們將該值 resolve(result.value)
,若是執行的過程當中出現錯誤,被 catch 住,咱們會將緣由 reject(e)
。
其次,咱們會使用 thunkToPromise
將回調函數包裝成一個 Promise,而後統一的添加 then 函數。在這裏值得注意的是,在 thunkToPromise
函數中,咱們遵循了 error first 的原則,這意味着當咱們處理回調函數的狀況時:
// 模擬數據請求 function fetchData(url) { return function(cb) { setTimeout(function() { cb(null, { status: 200, data: url }) }, 1000) } }
在成功時,第一個參數應該返回 null,表示沒有錯誤緣由。
咱們在第二版的基礎上將代碼寫的更加簡潔優雅一點,最終的代碼以下:
// 第三版 function run(gen) { return new Promise(function(resolve, reject) { if (typeof gen == 'function') gen = gen(); // 若是 gen 不是一個迭代器 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); } 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(ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError('You may only yield a function, promise ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }) } function isPromise(obj) { return 'function' == typeof obj.then; } function toPromise(obj) { if (isPromise(obj)) return obj; if ('function' == typeof obj) return thunkToPromise(obj); return obj; } function thunkToPromise(fn) { return new Promise(function(resolve, reject) { fn(function(err, res) { if (err) return reject(err); resolve(res); }); }); } module.exports = run;
若是咱們再將這個啓動器函數寫的完善一些,咱們就至關於寫了一個 co,實際上,上面的代碼確實是來自於 co……
而 co 是什麼? co 是大神 TJ Holowaychuk 於 2013 年 6 月發佈的一個小模塊,用於 Generator 函數的自動執行。
若是直接使用 co 模塊,這兩種不一樣的例子能夠簡寫爲:
// yield 後是一個 Promise var fetch = require('node-fetch'); var co = require('co'); function* gen() { var r1 = yield fetch('https://api.github.com/users/github'); var json1 = yield r1.json(); var r2 = yield fetch('https://api.github.com/users/github/followers'); var json2 = yield r2.json(); var r3 = yield fetch('https://api.github.com/users/github/repos'); var json3 = yield r3.json(); console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n')); } co(gen);
// yield 後是一個回調函數 var co = require('co'); function fetchData(url) { return function(cb) { setTimeout(function() { cb(null, { status: 200, data: url }) }, 1000) } } function* gen() { var r1 = yield fetchData('https://api.github.com/users/github'); var r2 = yield fetchData('https://api.github.com/users/github/followers'); console.log([r1.data, r2.data].join('\n')); } co(gen);
是否是特別的好用?
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。