這篇文章帶給大家的內容是關於ES6中Generator的自動執行詳解,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
單一非同步任務
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
為了獲得最終的執行結果,你需要這樣做:
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
首先執行Generator 函數,取得遍歷器對象。
接著使用 next 方法,執行非同步任務的第一階段,即 fetch(url)。
注意,由於fetch(url) 會傳回一個Promise 對象,所以result 的值為:
{ value: Promise { <pending> }, done: false }
最後我們為這個Promise 物件新增一個then 方法,先將其傳回的資料格式化(data.json()),再呼叫g.next,將所得的資料傳進去,由此可以執行非同步任務的第二階段,程式碼執行完畢。
多個非同步任務
上節我們只呼叫了一個接口,那如果我們呼叫了多個接口,使用了多個yield,我們豈不是要在then 函數中不斷的嵌套下去…
所以我們來看看執行多個非同步任務的情況:
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) });
但我知道你肯定不想寫成這樣…
其實,利用遞歸,我們可以這樣寫:
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 物件才能保證 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) } }
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); }); });
function run(gen) { var g = gen(); function next(data) { var result = g.next(data); if (result.done) return; result.value(next); } next(); } run(gen);
run
由此可以看到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;
return Promise
我們已經寫了一個不錯的啟動器函數,支援 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;
// 模拟数据请求 function fetchData(url) { return function(cb) { setTimeout(function() { cb(null, { status: 200, data: url }) }, 1000) } }
優化
我們在第二版的基礎上將程式碼寫的更簡潔優雅一點,最終的程式碼如下:// 第三版 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 是什么? 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);
是不是特别的好用?
以上是Generator在ES6中的自動執行詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!