寫在前面
鋪天蓋地的 Promise 文章還沒完全消散,Promise 本身就被 ES7 勸退了
關於 Promise 的具體語法及應用場景,可以查看:
-
[完全理解 Promise](/articles/完全理解 promise/)
-
[Promise 的適用場景](/articles/promise 的適用場景/)
至於另一篇([動手實現 promise](/articles/動手實現 promise/)),不建議看,這個實現存在的問題很多(性能、未知 bug 等等),也不打算繼續維護,千萬不要用
Promise 作為一種異步流程控制方法,本篇介紹其實現機制,並模擬實現基本功能
一。基礎部分
Promise 內部分為 Promise 對象與 Deferred 對象,前者提供接口接收 handler,後者記錄狀態,維持異步邏輯正確執行(男主外女主內的感覺,promise.then() 接收 handler,內部的 deferred 負責維護狀態)
Promise
Promise 主外,接收 handler 並註冊給事件機制
簡單情況下,Promise 可以直接套用 EventEmitter,如下:
var EventEmitter = require('events');
var util = require('util');
//--- 自定義 promise
var MyPromise = function() {
EventEmitter.call(this);
};
util.inherits(MyPromise, EventEmitter);
然後公開 then(),負責接收 handler:
MyPromise.prototype.then = function(onFulfilled, onRejected, onProgress) {
if (typeof onFulfilled === 'function') {
this.once('resolve', onFulfilled);
}
if (typeof onRejected === 'function') {
this.once('reject', onRejected);
}
if (typeof onProgress === 'function') {
this.on('progress', onProgress);
}
};
主外的就結束了,完全看不到將要成為 Promise 的端倪(現在確實不像,因為這個 Promise 不是最終暴露出去的 Promise)
Deferred
Deferred 要維護狀態,稍複雜一些
初始狀態,如下:
var Deferred = function() {
this.state = 'pending';
this.promise = new MyPromise();
};
Deferred 實例持有 promise 引用以便觸發事件執行 handler(主內的管著主外的)
然後把事件機制與狀態聯繫起來:
Deferred.prototype.resolve = function(res) {
this.promise.emit('resolve', res);
this.state = 'resolved';
};
Deferred.prototype.reject = function(err) {
this.promise.emit('reject', err);
this.state = 'rejected';
};
Deferred.prototype.progress = function(chunk) {
this.promise.emit('progress', chunk);
};
最基礎的部分完成了,最後提供一個 Promise 並暴露出去:
module.exports = {
Promise: MyPromise,
Deferred: Deferred,
MyPromise: function(fn) {
// 向 fn 注入 reslove 和 reject
var deferred = new Deferred();
fn.call(deferred, (res) => {
process.nextTick(() => {deferred.resolve(res);});
}, (err) => {
process.nextTick(() => {deferred.reject(err);});
});
return deferred.promise;
}
};
module.exports.MyPromise 就是最終產物,延遲執行 reslove/reject 是為了等待外部的 promise.then(),也就是先讓 handler 同步註冊,然後再觸發事件,這也是 Promise 不用預先指定分支及延遲邏輯處理的秘密所在
示例如下:
var P = require('./p.js');
var p1 = new P.MyPromise(function(resolve, reject) {
console.log('#1');
resolve(1);
console.log('#2');
});
p1.then((res) => {console.log(res);});
// log print:
// #1
// #2
// 1
var p2 = new P.MyPromise(function(resolve, reject) {
console.log('#1');
reject(new Error('reject'));
console.log('#2');
});
p2.then(null, (err) => {console.log(err.toString());});
// log print:
// #1
// #2
// Error: reject
二。擴展功能
1. 回調函數生成
Node 回調函數大都遵循 callback(err, res1, res...) 的規則,可以據此生成回調函數:
// node callback
Deferred.prototype.callback = function() {
return (err, value) => {
if (err) {
this.reject(err);
}
else if (arguments.length > 2) {
this.resolve(Array.prototype.slice.call(arguments, 1));
}
else {
this.resolve(value);
}
};
};
可以簡化 promisify 過程,讓 Promise 更好用一些
2. 多依賴異步控制
各個異步庫都提供了依賴控制方法,比如大而全的 async 模塊提供的 series(), parallel(), waterfall() 等等
Promise 提供的依賴控制不夠多,只支持 all(), race(),這裡模擬實現 all(),如下:
// 多依賴異步控制
Deferred.prototype.all = function(promises) {
var results = [];
var count = promises.length;
promises.forEach((promise, i) => {
promise.then((data) => {
results[i] = data;
count--;
if (count === 0) {
this.resolve(results);
}
}, (err) => {
this.reject(err);
});
});
return this.promise;
};
用法示例:
// 多依賴異步控制
var fs = require('fs');
// promisify
var readFile = function(file, encoding) {
encoding = encoding || 'utf-8';
var deferred = new P.Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
};
// test
var p1 = readFile('./p.js');
var p2 = readFile('./index.js');
var d1 = new P.Deferred();
d1.all([p1, p2]).then((arrRes) => {
var aRes = arrRes.map((res) => {
return res.toString().slice(0, 20);
});
console.log(aRes);
}, (err) => {
console.log(err);
});
// log print:
// [ 'var EventEmitter = r', 'var P = require(\'./p' ]
這裡沒有把 Deferred 包裝起來,更清晰一些(其實是懶得包裝了--)
3. Promise 鏈
支持 Promise 鏈需要維護一個隊列,並根據隊列中每個 promise 的狀態手動控制,偷懶的事件機制不再適用了(嗯,需要大改)
Promise
剔除事件機制,採用任務隊列
//--- 自定義 promise
var MyPromise = function() {
// 用於支持 promise 鏈
this.queue = [];
this.isPromise = true;
};
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// 採用任務隊列手動控制,不利用事件機制觸發執行了
var handler = {};
if (typeof onFulfilled === 'function') {
handler.onFulfilled = onFulfilled;
}
if (typeof onRejected === 'function') {
handler.onRejected = onRejected;
}
this.queue.push(handler);
return this;
};
Deferred
手動執行回調,並傳遞結果
var Deferred = function() {
this.state = 'pending';
this.promise = new MyPromise();
};
Deferred.prototype.resolve = function(res) {
var handler;
while (handler = this.promise.queue.shift()) {
if (handler && handler.onFulfilled) {
// 執行肯定回調
var ret = handler.onFulfilled(res);
// 如果肯定回調的返回值為 promise,更新 this.promise
if (ret && ret.isPromise) {
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
}
}
};
Deferred.prototype.reject = function(err) {
var handler;
while (handler = this.promise.queue.shift()) {
if (handler && handler.onRejected) {
var ret = handler.onRejected(err);
if (ret && ret.isPromise) {
ret.queue = this.promise.queue;
this.promise = ret;
return;
}
else {
// 把 ret 傳入下一個 handler 的 onFulfilled
var nextHandler = this.promise.queue.shift();
if (nextHandler && nextHandler.onFulfilled) {
nextHandler.onFulfilled(ret);
}
return;
}
}
}
};
用法示例如下:
// promise 鏈
var NewP = require('./newp.js');
// promisify
var readFile = function(file, encoding) {
encoding = encoding || 'utf-8';
var deferred = new NewP.Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
};
// test resolve
readFile('./p.js').then((data) => {
return readFile('./index.js');
}).then((data) => {
console.log('index: ' + data.slice(0, 20));
});
// log print:
// index: var P = require('./p
// test reject
readFile('./a.bcd').then((data) => {
return readFile('./p.js');
}, (err) => {
console.log('!!!error occurs: ' + err);
return 'something for next onFulfilled';
}).then((data) => {
console.log(data);
});
// log print:
// !!!error occurs: Error: ENOENT: no such file or directory, open...
// something for next onFulfilled
4. smooth
smooth 表示 promisify 現有 API,技巧是篡改 arguments,如下:
var smooth = (method) => {
return function() {
var deferred = new P.Deferred();
var args = Array.prototype.slice.call(arguments, 0);
args.push(deferred.callback());
method.apply(null, args);
return deferred.promise;
};
};
由 smooth 內部偷偷提供 callback,用法很簡潔:
var readFile = smooth(fs.readFile);
readFile('./index.js', 'utf-8').then((res) => {
console.log(res.slice(0, 20));
});
異步方法的回調函數從代碼中消失了。這個技巧對於「正版 Promise」同樣適用,需要用 Promise 包裝大量 API 時可以考慮
三。總結
Promise 在舞台上轉了一圈,然後沒能登上領獎台
作為異步流程控制方案,Promise 最大的問題就是存在封裝(promisify)成本,而且提供的多依賴控制方法不夠用,需要自行擴展,所以,不好用,不好用就不會長久
過時了就沒什麼好說的,還記得當時鋪天蓋地的《關於 Promise 你不知道的...》、《你真的會用 Promise 嗎》
參考資料
- 《深入淺出 NodeJS》
暫無評論,快來發表你的看法吧