跳到主要內容
黯羽輕揚每天積累一點點

模擬 Promise_Node 異步流程控制 3

免費2016-06-30#Node#Promise#node promise#promise原理#promise分析

Promise 在舞台上轉了一圈,然後沒能登上領獎台

寫在前面

鋪天蓋地的 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》

評論

暫無評論,快來發表你的看法吧

提交評論