Skip to main content

Simulating Promise_Node Asynchronous Flow Control 3

Free2016-06-30#Node#Promise#node promise#promise原理#promise分析

Promise went around on the stage, then failed to reach the podium

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"

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment