本文始發於我的個人部落格,如需轉載請註明出處。
為了更好的閱讀體驗,可以直接進去我的個人部落格看。
前言
知識儲備
閱讀本文需要對Generator
和Promise
有一個基本的瞭解。
這裡我簡單地介紹一下兩者的用法。
Generator
關於Generator的用法,推薦MDN上面的解釋function *函式,裡面非常詳細。
用一句話總結就是,generator函式
是回撥地獄的一種解決方案,它跟promise
類似,但是卻可以以同步的方式來書寫程式碼,而避免了promise的鏈式呼叫。
它的執行過程在於呼叫生成器函式(generator function)
後,會返回一個iterator(迭代)物件
,即Generator物件
,但是它並不會立刻執行裡面的程式碼。
它有幾個方法,next()
, throw()
和return()
。呼叫next()方法後,它會找到第一個yield關鍵字(直到找到程式底部或者return語句),每次程式執行到yield關鍵字時,程式便會暫停,儲存當前環境裡面的變數的值,然後可以跳出當前執行環境去執行yield後面的程式碼,再把結果返回回來。
返回的結果是一個物件,類似於{value: ``, done: false}
, value表示本次yield後面執行之後返回的結果。如果是Promise例項,則是返回resolved後的值。done表示迭代器是否執行完畢,若為true
,則表示當前生成器函式已經產生了最後輸出的值,即生成器函式已經返回。
下面是一個簡單的例子:
const gen = function *() {
let index = 0;
while(index < 3)
yield index++;
return `All done.`
};
const g = gen();
console.log(g.constructor); // output: GeneratorFunction {}
console.log(g.next()); // output: { value: 0, done: false }
console.log(g.next()); // output: { value: 1, done: false }
console.log(g.next()); // output: { value: 2, done: false }
console.log(g.next()); // output: { value: `All done.`, done: true }
console.log(g.next()); // output: { value: undefined, done: true }
Promise
關於Promise
的用法,可以查閱我之前寫過的一篇文章《關於ES6中Promise的用法》,寫得比較詳細。
Promise物件用於一個非同步操作的最終完成(或失敗)及其結果值的表示(簡單點說就是處理非同步請求)。Promise核心就在於裡面狀態的變換,是rejected
、resolved
還是pending
,還有就是原型鏈上的then()
方法,它可以傳遞本次狀態轉換後返回的值。
進入主題
由於實際需要,這幾天學習了koa2.x
框架,但是它已經不推薦使用generator函式了,推薦用async/await
組合。
koa2.x的最新用法:
async/await(node v7.6+):
const Koa = require(`koa`);
const app = new Koa();
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
common 用法:
const Koa = require(`koa`);
const app = new Koa();
// response
app.use(ctx => {
ctx.body = `Hello Koa`;
});
app.listen(3000);
由於本地的Node版本是v6.11.5
,而使用async/await則需要Node版本v7.6
以上,所以我想有沒有什麼模組能夠把koa2.x版本的語法相容koa1.x的語法。koa1.x語法的關鍵在於generator/yield
組合。通過yield可以很方便地暫停程式的執行,並改變執行環境。
這時候我找到了TJ大神寫的co模組
,它可以讓非同步流程同步化,還有koa-convert
模組等等,這裡著重介紹co模組。
co在koa2.x裡面的用法如下:
const Koa = require(`koa`);
const app = new Koa();
const co = require(`co`);
// response
app.use(co.wrap(function *(ctx, next) {
yield next();
// yield someAyncOperation;
// ...
ctx.body = `co`;
}));
app.listen(3000);
co模組不僅可以配合koa框架充當中介軟體的轉換函式使用,還支援批量執行generator函式,這樣就無需手動呼叫多次next()來獲取結果了。
它支援的引數有函式、promise、generator、陣列和物件
。
// co的原始碼
return onRejected(new TypeError(`You may only yield a function, promise, generator, array, or object, `
+ `but the following object was passed: "` + String(ret.value) + `"`));
下面舉一個co傳遞進來一個generator函式的例子:
// 這裡模擬一個generator函式呼叫
const co = require(`co`);
co(gen).then(data => {
// output: then: ALL Done.
console.log(`then: ` + data);
});
function *gen() {
let data1 = yield pro1();
// output: pro1 had resolved, data1 = I am promise1
console.log(`pro1 had resolved, data1 = ` + data1);
let data2 = yield pro2();
// output: pro2 had resolved, data2 = I am promise2
console.log(`pro2 had resolved, data2 = ` + data2);
return `ALL Done.`
}
function pro1() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 2000, `I am promise1`);
});
}
function pro2() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, `I am promise2`);
});
}
我覺得co()函式很神奇,裡面究竟經過了什麼樣的轉換?抱著一顆好奇心,讀了一下co的原始碼。
co原始碼分析
主要脈絡
co函式呼叫後,返回一個Promise例項。
co的思想就是將一個傳遞進來的引數進行合法化,再通過轉換成Promise例項返回出去。如果引數fn是generator函式
的話,裡面還可以自動進行遍歷,執行generator函式裡面的yield關鍵字後面的內容,並返回結果,也就是不斷地呼叫fn().next()
方法,再通過傳遞返回的Promise例項resolved
後的值,從而達到同步執行generator函式的效果。
這裡要注意,co裡面最主要的是要理解Promise例項和Generator物件,它們是co函式裡面的程式自動遍歷執行的關鍵。
下面解釋一下co模組裡面的最重要的兩部分,一個是generator函式的自動呼叫,另外一個是引數的Promise化。
第一,generator函式的自動呼叫(中文部分是我的解釋):
function co(gen) {
// 儲存當前的執行環境
var ctx = this;
// 切割出函式呼叫時傳遞的引數
var args = slice.call(arguments, 1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
// 返回一個Promise例項
return new Promise(function(resolve, reject) {
// 如果gen是一個函式,則返回一個新的gen函式的副本,
// 裡面繫結了this的指向,即ctx
if (typeof gen === `function`) gen = gen.apply(ctx, args);
// 如果gen不存在或者gen.next不是一個函式
// 就說明gen已經呼叫完成,
// 那麼直接可以resolve(gen),返回Promise
if (!gen || typeof gen.next !== `function`) return resolve(gen);
// 首次呼叫gen.next()函式,假如存在的話
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
// 嘗試著獲取下一個yield後面程式碼執行後返回的值
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 處理結果
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
// 嘗試丟擲錯誤
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
// 處理結果
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
// 這個next()函式是最為關鍵的一部分,
// 裡面幾乎包含了generator自動呼叫實現的核心
function next(ret) {
// 如果ret.done === true,
// 證明generator函式已經執行完畢
// 即已經返回了值
if (ret.done) return resolve(ret.value);
// 把ret.value轉換成Promise物件繼續呼叫
var value = toPromise.call(ctx, ret.value);
// 如果存在,則把控制權交給onFulfilled和onRejected,
// 實現遞迴呼叫
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) + `"`));
}
});
}
對於以上程式碼中的onFulfilled
和onRejected
,我們可以把它們看成是co模組對於resolve
和reject
封裝的加強版。
第二,引數Promise化,我們來看一下co中的toPromise的實現:
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if (`function` == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
toPromise的本質上就是通過判定引數的型別,然後再通過轉移控制權給不同的引數處理函式,從而獲取到期望返回的值。
關於引數的型別的判斷,看一下原始碼就能理解了,比較簡單。
我們著重來分析一下objectToPromise的實現:
function objectToPromise(obj){
// 獲取一個和傳入的物件一樣構造器的物件
var results = new obj.constructor();
// 獲取物件的所有可以遍歷的key
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// 對於陣列的每一個項都呼叫一次toPromise方法,變成Promise物件
var promise = toPromise.call(this, obj[key]);
// 如果裡面是Promise物件的話,則取出e裡面resolved後的值
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
// 並行,按順序返回結果,返回一個陣列
return Promise.all(promises).then(function () {
return results;
});
// 根據key來獲取Promise例項resolved後的結果,
// 從而push進結果陣列results中
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
上面理解的關鍵就在於把key遍歷,如果key
對應的value
也是Promise
物件的話,那麼呼叫defer()
方法來獲取resolved
後的值。
編寫自己的generator函式執行器
通過以上的簡單介紹,我們就可以嘗試來寫一個屬於自己的generator函式執行器了,目標功能是能夠自動執行function*
函式,並且裡面的yield子句
後面跟著的都是Promise例項
。
具體程式碼(my-co.js
)如下:
// my-co.js
module.exports = my-co;
let my-co = function (gen) {
// gen是一個具有Promise的生成器函式
const g = gen(); // 迭代器
// 首次呼叫next
next();
function next(val) {
let ret = g.next(val); // 呼叫ret
if (ret.done) {
return ret.value;
}
if (ret && `function` === typeof ret.value.then) {
ret.value.then( (data) => {
// 繼續迴圈下去
return next(data); // promise resolved
});
}
}
};
這樣我們就可以在test.js
檔案中呼叫了:
// test.js
const myCo = require(`./my-co`);
const fs = require(`fs`);
let gen = function *() {
let data1 = yield pro1();
console.log(`data1: ` + data1);
let data2 = yield pro2();
console.log(`data2: ` + data2);
let data3 = yield pro3();
console.log(`data3: ` + data3);
let data4 = yield pro4(data1 + `
` + data2 + `
` + data3);
console.log(`data4: ` + data4);
return `All done.`
};
// 呼叫myCo
myCo(gen);
// 延遲兩秒resolve
function pro1() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 2000, `promise1 resolved`);
});
}
// 延遲一秒resolve
function pro2() {
return new Promise((resolve, reject) => {
setTimeout(resolve, 1000, `promise2 resolved`);
});
}
// 寫入Hello World到./1.txt檔案中
function pro3() {
return new Promise((resolve, reject) => {
fs.appendFile(`./1.txt`, `Hello World
`, function(err) {
resolve(`write-1 success`);
});
});
}
// 寫入content到./1.txt檔案中
function pro4(content) {
return new Promise((resolve, reject) => {
fs.appendFile(`./1.txt`, content, function(err) {
resolve(`write-2 success`);
});
});
}
控制檯輸出結果:
// output
data1: promise1 resolved
data2: promise2 resolved
data3: write-1 success
data4: write-2 success
./1.txt
檔案內容:
Hello World
promise1 resolved
promise2 resolved
write-1 success
由上可知,執行的結果符合我們的期望。
雖然這個執行器很簡單,後面只支援Promise例項,並且也不支援多種引數,但是卻引匯出了一個思路,促使我們思考怎麼去展示我們的程式碼,還有就是很有效地避免了多重then,以同步的方式來書寫非同步程式碼。Promise解決的是回撥地獄
的問題(callback hell
),而Generator解決的是程式碼的書寫方式。孰優孰劣,全在於個人意願。
總結
以上分析了co部分原始碼的精髓,講到了co函式裡面generator函式自動遍歷執行的機制,還講到了co裡面最為關鍵的objectToPromise()
方法。
在文章的後面我們編寫了一個屬於自己的generator函式遍歷器,其中主要的是next()方法,它可以檢測我們yield後面Promise操作是否完成。如果generator的狀態done
還沒有置為true
,那麼繼續呼叫next(val)
方法,並把上一次yield
操作獲取到的值傳遞下去。
有時候在引用別人的模組出現問題時,如果在網上找不到自己期望的答案,那麼我們可以根據自己的能力來選擇性地分析一下作者的原始碼,看原始碼是一種很好的成長方式。
坦白說,這是我第一次深入分析模組的原始碼,co模組的原始碼包括註釋和空行只有230多行
左右,所以這是一個很好的切入點。裡面程式碼雖少,但是理解卻不易。
如果以上所述有什麼問題,歡迎反饋。
感謝支援。