서론
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》
아직 댓글이 없습니다