メインコンテンツへ移動

Promise の模擬_Node 非同期フロー制御 3

無料2016-06-30#Node#Promise#node promise#promise原理#promise分析

Promise はステージ上で一回転しましたが、表彰台に登ることはできませんでした

はじめに

Promise に関する記事が溢れていたのもつかの間、Promise 自体が ES7 で劝退されました

Promise の具体的な構文及び応用シナリオについては、以下を参照できます:

  • [Promise を完全に理解](/articles/完全理解 promise/)

  • [Promise の適用シナリオ](/articles/promise の適用シナリオ/)

もう 1 篇([动手实现 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 は既存 API を promisify することを表し、コツは 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」にも適用し、大量の API を Promise で包装する必要がある時に考慮できます

三.まとめ

Promise はステージ上で一回転しましたが、表彰台に登ることはできませんでした

非同期フロー制御方案として、Promise の最大の問題は封装(promisify)コストが存在すること、そして提供する多依存制御方法が十分でなく、自行拡張する必要があることです。そのため、使いにくく、使いにくければ長続きしません

時代遅れになれば言うことはありません。当時溢れていた《关于 Promise你不知道の...》、《你真的会用Promise吗》を覚えていますか

参考資料

  • 《深入浅出 NodeJS》

コメント

コメントはまだありません

コメントを書く