【转向JavaScript系列】深入理解Promise
本文缘起前段时间一朋友换工作时,笔试题中要求手写一个Promise。在工作中虽已大量使用Promise,其原理却没有深入探究过,换做自己,当场也很难手写一个完善的Promise实现。近期补了一些课,以本文来彻底的理解Promise实现原理。
1.Promise是什么
Promise是抽象异步处理对象以及对其进行各种操作的组件,可以将复杂的异步处理轻松的进行模式化。
使用Promise进行异步处理的一个例子
function getUserId() {
return new Promise(function (resolve, reject) {
// 异步请求
Y.io('/userid/1', {
on: {
success: function (id, res) {
var o = JSON.parse(res);
if (o.status === 1) {
resolve(o.id);
} else {
// 请求失败,返回错误信息
reject(o.errorMsg);
}
}
}
});
});
}
getUserId().then(function (id) {
// do sth with id
}, function (error) {
console.log(error);
});
如对Promise的使用尚不了解,推荐阅读JavaScript Promise Cookbook中文版
在掌握了Promise的使用后,推荐继续阅读Promise规范Promise A+ 规范
2.Promise实现原理
理解实现原理过程中,阅读了不少相关文章,其中剖析 Promise 之基础篇和JS Promise的实现原理这两篇文章,个人认为质量较高。本文对原理的理解,也是基于这两篇文章。
借用MDN的图,看看Promise的状态迁移,每个 Promise 存在三个互斥状态:pending、fulfilled、rejected,它们之间的关系是:
2.1 Promise简易实现
最初我是阅读剖析 Promise 之基础篇来学习的,文中初始实现了一个简易版的Promise
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled) {
if (state === 'pending') {
deferreds.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
deferreds.forEach(function (deferred) {
deferred(value);
});
}, 0);
}
fn(resolve);
}
function getUserId() {
return new Promise(function (resolve) {
resolve(123);
});
}
getUserId().then(function (id) {
console.log('do sth with', id);
});
这一版本的实现,还是很好理解的。
- Promise初始状态为pending、value为null、延迟队列为空。并且作为函数作用域的变量,不向外暴露
- 传入Promise的函数fn立即执行,将resolve传入fn,fn执行完成调用resolve
- resolve被定义为一个内部函数,使用闭包方式来访问value、state、deferreds。遵循Promise规范,resolve方法中,采用异步方式执行延迟队列的方法
- promise对象上添加then方法,当前promise对象状态为pending时,将通过then方法注册的新方法,添加到延迟队列;当前promise对象状态为完成时,执行注册的方法
2.2 串行Promise
上述简易版本的实现,相信理解起来无压力。但是在理解剖析 Promise 之基础篇文中串行Promise时,着实费了一番脑筋。
function getUserId() {
return new Promise(function (resolve) {
window.setTimeout(function () {
resolve(9876);
});
});
}
function getUserMobileById(id) {
return new Promise(function (resolve) {
console.log('start to get user mobile by id:', id);
window.setTimeout(function () {
resolve(13810001000);
});
});
}
getUserId()
.then(getUserMobileById)
.then(function (mobile) {
console.log('do sth with', mobile);
});
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}
var ret = deferred.onFulfilled(value);
deferred.resolve(ret);
}
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
deferreds.forEach(function (deferred) {
handle(deferred);
});
}, 0);
}
fn(resolve);
}
初看上述代码时,跟着原文叙述,没能彻底理解其执行流程。通过不断的断点调试,才最终理解。在这一过程中,明白了下面几点
- Promise的执行过程可以分为两个阶段,即初始时注册阶段和完成时的resolve阶段
- 初始时,通过promise.then注册的方法,保存在promise对象的延迟队列。每次调用then方法,返回一个新promis实例,作为链式调用的桥接,这类promise可以乘坐bridge promise
- 注册的方法执行完,执行resolve时。从当前promise对象延迟队列取出注册的方法继续执行。当注册的方法生成一个新promise实例时,调用then方法注册到对应的延迟队列中;否则依次resolve链式调用中相应的promise实例
将上述过程表达如下
理解上述执行流程,再为上述实现加上错误执行过程和异常处理
function Promise(fn) {
var state = 'pending',
value = null,
deferreds = [];
this.then = function (onFulfilled, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};
function handle(deferred) {
if (state === 'pending') {
deferreds.push(deferred);
return;
}
var cb = state === 'fulfilled' ? deferred.onFulfilled : deferred.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? deferred.resolve : deferred.reject;
cb(value);
return;
}
try {
ret = cb(value);
deferred.resolve(ret);
} catch (e) {
deferred.reject(e);
}
}
function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve, reject);
return;
}
}
state = 'fulfilled';
value = newValue;
finale();
}
function reject(reason) {
state = 'rejected';
value = reason;
finale();
}
function finale() {
setTimeout(function () {
deferreds.forEach(function (deferred) {
handle(deferred);
});
}, 0);
}
fn(resolve, reject);
}
3.更好的实现
通过阅读剖析 Promise 之基础篇,跟进上述代码执行过程,相信已经可以理解Promise实现原理。
在理解过程中,让我思考,上述执行流程到底是哪里不易理解,有没有更好的实现呢?
在上述的实现中,关键方法resolve被定义在Promise中作为内部函数,通过闭包获取promise对象的变量的引用。再将回调函数和resolve方法注册到延迟队列,通过resolve完成了链式的回调。这一过程隐式调用太多,不好理解。
继续阅读其他文章,认为这篇JS Promise的实现原理中有更好的实现。其完整实现可以查看这里。
3.1 代码结构
构造函数定义如下
function Promise(resolver) {
this._status = 'pending';
this._doneCallbacks = [];
this._failCallbacks = [];
resolver(resolve, reject);
...
}
在 promise 对象中定义了成功回调和失败回调的两个数组,数组中的每一项是通过内部封装的闭包函数调用的结果,也是一个函数。这个函数可以访问到内部调用闭包时传递的 promise 对象,因此通过这种方式也可以访问到我们需要的下一个 promise 以及其关联的成功、失败回调的引用。在then方法中增加闭包调用以及为前一个 promise 对象保存引用。
Promise.prototype.then = function(onResolve, onReject) {
var promise = new Promise(function() {});
this._doneCallbacks.push(makeCallback(promise, onResolve, 'resolve'));
this._failCallbacks.push(makeCallback(promise, onReject, 'reject'));
return promise;
}
then方法中调用的makeCallback即上面说到的闭包函数。调用时会把 promise 对象以及相应的回调传递进去,返回一个新的函数。前一个 promise 对象持有返回函数的引用,这样在调用返回函数时,在函数内部就可以访问到 promise 对象以及回调函数了。
function makeCallback(promise, callback, action) {
return function promiseCallback(value) {
...
};
}
resolve和reject函数的实现相对简单
function resolve(promise, data) {
if (promise._status !== 'pending') {
return;
}
promise._status = 'fullfilled';
promise._value = data;
run(promise);
}
function reject(promise, reason) {
if (promise._status !== 'pending') {
return;
}
promise._status = 'rejected';
promise._value = reason;
run(promise);
}
function run(promise) {
// `then`方法中也会调用,所以此处仍需做一次判断
if (promise._status === 'pending') {
return;
}
var value = promise._value;
var callbacks = promise._status === 'fullfilled'
? promise._doneCallbacks
: promise._failCallbacks;
// Promise需要异步操作
setTimeout(function () {
for (var i = 0, len = callbacks.length; i < len; i++) {
callbacks[i](value);
}
});
// 每个promise只能被执行一次。虽然`_doneCallbacks`和`_failCallbacks`用户不应该直接访问,
// 但还是可以访问到,保险起见,做清空处理。
promise._doneCallbacks = [];
promise._failCallbacks = [];
}
3.2 完整实现
除上述基本方法外,文中还实现了常用静态方法如Promise.all,Promise.race,完整代码如下
/**
* Promise对象的内部状态
*
* @type {Object}
*/
var Status = {
PENDING: 'pending',
FULLFILLED: 'resolved',
REJECTED: 'rejected'
};
function empty() {}
/**
* Promise构造函数
*
* @constructor
* @param {Function} resolver 此Promise对象管理的任务
*/
function Promise(resolver) {
// ES6原生的Promise构造函数中,若不通过`new`调用Promise的构造函数,会抛出TypeError异常。此处与其一致
if (!(this instanceof Promise)) {
throw new TypeError('TypeError: undefined is not a promise');
}
// ES6原生的Promise构造函数中,若无作为函数的resolver参数,会抛出TypeError异常。此处与其一致
if (typeof resolver !== 'function') {
throw new TypeError('TypeError: Promise resolver undefined is not a function');
}
/**
* Promise对象内部的状态,初始为`pending`。状态只能由`pending`到`fullfilled`或`rejected`
*
* @type {string}
*/
this._status = Status.PENDING;
/**
* Promise对象resolved/rejected后拥有的data/reason
*
* - 此处保存此值是为了当一个Promise对象被resolved或rejected后,继续对其调用`then`添加任务,后续处理仍能获得当前Promise的值
*
* @type {Mixed}
*/
this._value;
/**
* 当前Promise被resolved/rejected后,需处理的任务
*
* - 由于同一个Promise对象可以调用多次`then`方法,以添加多个并行任务,所以此处是一个数组
*
* @type {Array.<Function>}
*/
this._doneCallbacks = [];
this._failCallbacks = [];
var promise = this;
resolver(
function (data) {
resolve(promise, data);
},
function (reason) {
reject(promise, reason);
}
);
}
Promise.prototype = {
constructor: Promise,
/**
* Promise的`then`方法
*
* @param {Function|Mixed} onResolve 当前Promise对象被resolved后,需处理的任务
* @param {Function|Mixed} onReject 当前Promise对象被rejected后,需处理的任务
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
then: function (onResolve, onReject) {
var promise = new Promise(empty);
this._doneCallbacks.push(makeCallback(promise, onResolve, 'resolve'));
this._failCallbacks.push(makeCallback(promise, onReject, 'reject'));
// 如果在一个已经被fullfilled或rejected的promise上调用then,则需要直接执行通过then注册的回调函数
run(this);
return promise;
},
/**
* Promise的`done`方法
*
* @param {Function|Mixed} onResolve 当前Promise对象被resolved后,需处理的任务
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
done: function (onResolve) {
return this.then(onResolve, null);
},
/**
* Promise的`fail`方法
*
* @param {Function|Mixed} onReject 当前Promise对象被rejected后,需处理的任务
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
fail: function (onReject) {
return this.then(null, onReject);
},
/**
* Promise的`catch`方法
*
* @param {Function|Mixed} onFail 当前Promise对象被rejected后,需处理的任务
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
catch: function (onFail) {
return this.then(null, onFail);
}
};
/**
* 创建一个Promise对象,并用给定值resolve它
*
* @param {Mixed} value 用于resolve新创建的Promise对象的值
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
Promise.resolve = function (value) {
var promise = new Promise(empty);
resolve(promise, value);
return promise;
};
/**
* 创建一个Promise对象,并用给定值reject它
*
* @param {Mixed} reason 用于reject新创建的Promise对象的值
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
Promise.reject = function (reason) {
var promise = new Promise(empty);
reject(promise, reason);
return promise;
};
/**
* 返回一个promise,这个promise在iterable中的任意一个promise被解决或拒绝后,
* 立刻以相同的解决值被解决或以相同的拒绝原因被拒绝
*
* @param {Iterable.<Promise|Mixed>} iterable 一组Promise对象或其它值
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
Promise.race = function (iterable) {
if (!iterable || !iterable.hasOwnProperty('length')) {
throw new TypeError('TypeError: Parameter `iterable` must be a iterable object');
}
var promise = new Promise(empty);
for (var i = 0, len = iterable.length; i < len; i++) {
var iterate = iterable[i];
if (!(iterate instanceof Promise)) {
iterate = Promise.resolve(iterate);
}
iterate.then(resolveRaceCallback, rejectRaceCallback);
}
var settled = false;
function resolveRaceCallback(data) {
if (settled) {
return;
}
settled = true;
resolve(promise, data);
}
function rejectRaceCallback(reason) {
if (settled) {
return;
}
settled = true;
reject(promise, reason);
}
};
/**
* 返回一个promise,该promise会在iterable参数内的所有promise都被解决后被解决
*
* @param {Iterable.<Promise|Mixed>} iterable 一组Promise对象或其它值
* @return {Promise} 返回一个新的Promise对象,用于链式操作
*/
Promise.all = function (iterable) {
if (!iterable || !iterable.hasOwnProperty('length')) {
throw new TypeError('TypeError: Parameter `iterable` must be a iterable object');
}
var promise = new Promise(empty);
var length = iterable.length;
for (var i = 0; i < length; i++) {
var iterate = iterable[i];
if (!(iterate instanceof Promise)) {
iterate = Promise.resolve(iterate);
}
iterate.then(makeAllCallback(iterate, i, 'resolve'), makeAllCallback(iterate, i, 'reject'));
}
var result = [];
var count = 0;
function makeAllCallback(iterate, index, action) {
return function (value) {
if (action === 'reject') {
reject(promise, value);
return;
}
result[index] = value;
if (++count === length) {
resolve(promise, result);
}
}
}
};
/**
* 返回一个Deferred对象,包含一个新创建的Promise对象,以及`resolve`和`reject`方法
*
* @return {Deferred}
*/
Promise.defer = function () {
var promise = new Promise(empty);
return {
promise: promise,
resolve: function (data) {
resolve(promise, data);
},
reject: function (reason) {
reject(promise, reason);
}
};
};
function run(promise) {
// `then`方法中也会调用,所以此处仍需做一次判断
if (promise._status === Status.PENDING) {
return;
}
var value = promise._value;
var callbacks = promise._status === Status.FULLFILLED
? promise._doneCallbacks
: promise._failCallbacks;
// Promise需要异步操作
setTimeout(function () {
for (var i = 0, len = callbacks.length; i < len; i++) {
callbacks[i](value);
}
});
// 每个promise只能被执行一次。虽然`_doneCallbacks`和`_failCallbacks`用户不应该直接访问,
// 但还是可以访问到,保险起见,做清空处理。
promise._doneCallbacks = [];
promise._failCallbacks = [];
}
function resolve(promise, data) {
if (promise._status !== Status.PENDING) {
return;
}
promise._status = Status.FULLFILLED;
promise._value = data;
run(promise);
}
function reject(promise, reason) {
if (promise._status !== Status.PENDING) {
return;
}
promise._status = Status.REJECTED;
promise._value = reason;
run(promise);
}
function makeCallback(promise, callback, action) {
return function promiseCallback(value) {
// 如果传递了callback,则使用前一个promise传递过来的值作为参数调用callback,
// 并根据callback的调用结果来处理当前promise
if (typeof callback === 'function') {
var x;
try {
x = callback(value);
}
catch (e) {
// 如果调用callback时抛出异常,则直接用此异常对象reject当前promise
reject(promise, e);
}
// 如果callback的返回值是当前promise,为避免造成死循环,需要抛出异常
// 根据Promise+规范,此处应抛出TypeError异常
if (x === promise) {
var reason = new TypeError('TypeError: The return value could not be same with the promise');
reject(promise, reason);
}
// 如果返回值是一个Promise对象,则当返回的Promise对象被resolve/reject后,再resolve/reject当前Promise
else if (x instanceof Promise) {
x.then(
function (data) {
resolve(promise, data);
},
function (reason) {
reject(promise, reason);
}
);
}
else {
var then;
(function resolveThenable(x) {
// 如果返回的是一个Thenable对象(此处逻辑有点坑,参照Promise+的规范实现)
if (x && (typeof x === 'object'|| typeof x === 'function')) {
try {
then = x.then;
}
catch (e) {
reject(promise, e);
return;
}
if (typeof then === 'function') {
// 调用Thenable对象的`then`方法时,传递进去的`resolvePromise`和`rejectPromise`方法(及下面的两个匿名方法)
// 可能会被重复调用。但Promise+规范规定这两个方法有且只能有其中的一个被调用一次,多次调用将被忽略。
// 此处通过`invoked`来处理重复调用
var invoked = false;
try {
then.call(
x,
function (y) {
if (invoked) {
return;
}
invoked = true;
// 避免死循环
if (y === x) {
throw new TypeError('TypeError: The return value could not be same with the previous thenable object');
}
// y仍有可能是thenable对象,递归调用
resolveThenable(y);
},
function (e) {
if (invoked) {
return;
}
invoked = true;
reject(promise, e);
}
);
}
catch (e) {
// 如果`resolvePromise`和`rejectPromise`方法被调用后,再抛出异常,则忽略异常
// 否则用异常对象reject此Promise对象
if (!invoked) {
reject(promise, e);
}
}
}
else {
resolve(promise, x);
}
}
else {
resolve(promise, x);
}
}(x));
}
}
// 如果未传递callback,直接用前一个promise传递过来的值resolve/reject当前Promise对象
else {
action === 'resolve'
? resolve(promise, value)
: reject(promise, value);
}
};
}
4.最后
相比,剖析 Promise 之基础篇,JS Promise的实现原理文中代码结构要更加清晰,对闭包的调用更为易读。
像resolve(),reject(),run(),makeCallback()这几个工具方法,没有直接定义到Promise构造函数中,作为内部函数,产生隐式的闭包调用,而是提出来作为公共方法,通过参数传递方式将promise对象传入方法。使得代码可读性好,容易理解。
同时,在学习的过程中,读到这篇奇舞团翻译的Bluebird 是如何做到比原生实现更快的?
文中提到了Bluebird相对于原生Promise的几个优化点
- 函数中的对象分配最小化
- 减小对象体积
- 可选特性懒重写
其第一点就是避免在Promise构造函数中,直接定义其他内部函数。除开前面提到的可读性问题,避免这样做,可以避免每次创建Promise对象时,同时创建全新的内部函数对象。
参考文章
- JavaScript Promise Cookbook中文版
- Promise A+ 规范
- 剖析 Promise 之基础篇
- JS Promise的实现原理
- Bluebird 是如何做到比原生实现更快的?
- 剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类
- 细嗅promise
如果觉得有帮助,可以扫描二维码对我打赏,谢谢