Preface
The flood of Promise articles hasn't completely dissipated, yet Promise itself was discouraged by ES7
For specific syntax and application scenarios of Promise, you can check:
-
[Fully Understanding Promise](/articles/完全理解 promise/)
-
[Applicable Scenarios of Promise](/articles/promise 的适用场景/)
As for another article ([Implementing Promise by Hand](/articles/动手实现 promise/)), not recommended to read, this implementation has many problems (performance, unknown bugs, etc.), and there's no plan to continue maintenance, definitely don't use it
As an asynchronous flow control method, this article introduces Promise's implementation mechanism and simulates basic functionality
1. Basic Parts
Promise is internally divided into Promise object and Deferred object, the former provides interfaces to receive handlers, the latter records state, maintaining correct execution of asynchronous logic (feeling like man handles outside, woman handles inside, promise.then() receives handler, internal deferred responsible for maintaining state)
Promise
Promise handles outside, receives handlers and registers them to event mechanism
In simple cases, Promise can directly use EventEmitter, as follows:
var EventEmitter = require('events');
var util = require('util');
//--- Custom promise
var MyPromise = function() {
EventEmitter.call(this);
};
util.inherits(MyPromise, EventEmitter);
Then expose then(), responsible for receiving handlers:
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);
}
};
The outside handling is done, completely can't see the slightest hint of becoming Promise (indeed doesn't look like it now, because this Promise is not the final exposed Promise)
Deferred
Deferred needs to maintain state, slightly more complex
Initial state, as follows:
var Deferred = function() {
this.state = 'pending';
this.promise = new MyPromise();
};
Deferred instance holds promise reference to trigger events and execute handlers (inside handles outside)
Then connect event mechanism with state:
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);
};
The most basic part is done, finally provide a Promise and expose it:
module.exports = {
Promise: MyPromise,
Deferred: Deferred,
MyPromise: function(fn) {
// Inject resolve and reject into fn
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 is the final product, delaying execution of resolve/reject is to wait for external promise.then(), that is, let handlers register synchronously first, then trigger events, this is also the secret of Promise not needing to pre-specify branches and delayed logic processing
Example as follows:
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
2. Extended Features
1. Callback Function Generation
Node callback functions mostly follow callback(err, res1, res...) rule, can generate callback functions based on this:
// 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);
}
};
};
Can simplify promisify process, make Promise easier to use
2. Multi-Dependency Asynchronous Control
Various asynchronous libraries provide dependency control methods, such as the comprehensive async module providing series(), parallel(), waterfall(), etc.
Promise provides not enough dependency control, only supports all(), race(), here simulate implementation of all(), as follows:
// Multi-dependency asynchronous control
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;
};
Usage example:
// Multi-dependency asynchronous control
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' ]
Here didn't wrap Deferred, clearer (actually too lazy to wrap--)
3. Promise Chain
Supporting Promise chain needs to maintain a queue, and manually control based on each promise's state in the queue, lazy event mechanism no longer applies (yes, needs major changes)
Promise
Remove event mechanism, adopt task queue
//--- Custom promise
var MyPromise = function() {
// For supporting promise chain
this.queue = [];
this.isPromise = true;
};
MyPromise.prototype.then = function(onFulfilled, onRejected) {
// Adopt task queue for manual control, no longer use event mechanism to trigger execution
var handler = {};
if (typeof onFulfilled === 'function') {
handler.onFulfilled = onFulfilled;
}
if (typeof onRejected === 'function') {
handler.onRejected = onRejected;
}
this.queue.push(handler);
return this;
};
Deferred
Manually execute callbacks, and pass results
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) {
// Execute fulfillment callback
var ret = handler.onFulfilled(res);
// If fulfillment callback returns promise, update 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 {
// Pass ret to next handler's onFulfilled
var nextHandler = this.promise.queue.shift();
if (nextHandler && nextHandler.onFulfilled) {
nextHandler.onFulfilled(ret);
}
return;
}
}
}
};
Usage example as follows:
// promise chain
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 represents promisify existing API, trick is to tamper with arguments, as follows:
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 internally secretly provides callback, usage is very concise:
var readFile = smooth(fs.readFile);
readFile('./index.js', 'utf-8').then((res) => {
console.log(res.slice(0, 20));
});
Callback function of asynchronous method disappears from code. This trick also applies to "genuine Promise", can consider when needing to wrap大量 APIs with Promise
3. Summary
Promise went around on the stage, then failed to reach the podium
As an asynchronous flow control solution, Promise's biggest problem is existence of wrapping (promisify) cost, and provided multi-dependency control methods are not enough, need self-extension, so, not easy to use, not easy to use won't last long
When outdated, nothing much to say, still remember the flood of "What You Don't Know About Promise...", "Do You Really Know How to Use Promise" at that time
References
- "Deep Dive NodeJS"
No comments yet. Be the first to share your thoughts.