본문으로 건너뛰기

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 는 기존 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》

댓글

아직 댓글이 없습니다

댓글 작성