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 系列目錄地址:github.com/mqyqingfeng…
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。