字数:2874
阅读时间:10分钟
前言
promise是异步调用的解决方案,它有业界公认的标准:www.ituring.com.cn/article/665… 有许多优秀的实现插件,如:Jquery、Q等,ES6中也添加了该特性。
它的使用方式也很简单,如下代码:
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve('success');
},1000);
});
promise.then(function(response){
console.log(response);
});
//也可以使用defer语法糖,省略创建Promise的过程
let defer = Promise.defer();
setTimeout(function(){
defer.resolve('success');
},1000);
defer.promise.then(function(response){
console.log(response);
})
复制代码
但是如果我们想知道更多的内容,如:
1.promise中的执行器参数什么时候执行的。
2.then函数中的回调函数什么时候执行。
3.链式调用then函数时,它的执行顺序是什么样的;如果有一个promise被拒绝了,后续如何调用;如果代码出现异常,后续then函数如何调用;如果上一个回调函数返回了一个promise对象,又是如何运行的...
这时,最好的办法莫过于查看实现源码了,但更好的办法是我们自己去实现一次。因此,本文我会与大伙一起基于A+规范实现一个自定义的Promise,旨在更好地理解promise的用法。
正文
先上标准,
中文:www.ituring.com.cn/article/665…
一、实现一个promise类
基于上述标准,理清思路:
1.我们需要实现一个Promise类,其构造函数接受一个执行器参数,并且会在构造函数中调用该执行器。
2.Promise应有当前状态、终值、原因三个属性。
3.Promise应该实现执行、拒绝两个动作。
4.由于then是可以多次调用的,所以,Promise中应该有存储执行动作和拒绝动作的两个队列。
5.Promise应该暴露一个then函数,用来处理回调函数。
按照上述思路,我们编写出如下代码:
/**
* 自定义promise对象
*/
class Promise {
/**
* 构造函数
* @param {Function} excutor 执行器
*/
constructor (excutor) {
//promise状态,有pending、resolved、rejected
this.status = 'pending';
//终值
this.value;
//拒因
this.reason;
//解决函数队列
this.resolveFuns = [];
//拒绝函数队列
this.rejectFuns = [];
//解决函数
let resolve = val => {
if (this.status === 'pending') {
this.status = 'resolved';
this.value = val;
this.resolveFuns.forEach(func => func());
}
};
//拒绝函数
let reject = reason => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.rejectFuns.forEach(func => func());
}
};
try {
excutor(resolve, reject);
} catch (ex) {
reject(ex);
}
}
/**
* 回调函数处理
* @param {Function} resolveCallBack 执行回调函数
* @param {Function} rejectCallBack 拒绝回调函数
*/
then (resolveCallBack, rejectCallBack) {
}
}
复制代码
参照规范:
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
这里我们status
属性对应如上三种状态(我们更习惯使用resolved来表示执行,因此没有使用fulfilled)。
构造函数中,resolve和reject函数的实现中,遵循了规范中对状态变化的规定:
等待态(Pending)
处于等待态时,promise 需满足以下条件:
- 可以迁移至执行态或拒绝态
执行态(Fulfilled)
处于执行态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的终值
拒绝态(Rejected)
处于拒绝态时,promise 需满足以下条件:
- 不能迁移至其他任何状态
- 必须拥有一个不可变的据因
如果执行器函数执行报错,则直接执行拒绝动作。因此,如果我们使用promise时,传入的执行器执行报错,则会在then函数中的拒绝回调函数中捕获到错误。但是,如果是执行器函数中的延时操作报错(例setTimeout中报错),这里是无法捕获到错误的。
二、实现then函数
基于标准,理清思路:
1.then函数接受两个参数:执行回调函数和拒绝回调函数。如果传入的参数并非函数,则忽略它并将信息传递到下一个then函数中。
2.then函数必须返回一个新的promise。
3.如果promise状态为pending,则应该将回调函数推入队列;如果状态为resolved,则应直接调用执行回调函数;如果状态为reject,则应直接调用拒绝回调函数。如果回调函数执行报错,则应该执行下一个then函数中的决绝回调函数。
4.需要处理then的链式调用。
5.所有的回调函数都应该异步执行(为了保证then函数执行顺序的一致性。)
首先,我们编写一个对应参数和返回值的函数,代码如下:
/**
* 回调函数处理
* @param {Function} resolveCallBack 执行回调函数
* @param {Function} rejectCallBack 拒绝回调函数
* @return {promise} 新创建的promise
*/
then (resolveCallBack, rejectCallBack) {
let pDeffer = Promise.defer();
if (this.status === 'pending') {
//如果promise状态为pending则将回调函数加入队列中
} else if (this.status === 'resolved') {
//如果promise状态为resolved,则立即执行resolveCallBack回调函数,
//如此则无论promise是否已经执行完毕,回调函数都必然会执行
} else if (this.status === 'rejected') {
//如果promise状态为rejected,则立即执行rejectedCallBack回调函数,
//原因同上
}
return pDeffer.promise;
}
复制代码
其中defer函数是我在Promise对象上创建的一个静态函数,是一个创建Promise对象的语法糖函数,其代码如下:
/**
* 创建一个默认的promise(语法糖)
* @return {Object} deffer对象
*/
Promise.defer = Promise.deferred = () => {
let defer = {};
defer.promise = new Promise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
};
复制代码
然后根据整理的思路1中,需要处理参数不为函数的情况。执行回调函数直接返回接受的返回值即可,拒绝回调函数抛出对应原因的异常即可(这里只有抛出异常,下一个then函数才会调用拒绝回调函数)。得到如下代码:
then (resolveCallBack, rejectCallBack) {
//如果resolveCallBack不是函数,则将值传递到下一个resolveCallBack函数中
resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;
//如果rejectCallBack不是函数,则将原因传递到下一个rejectCallBack函数中
rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
throw reason;
};
...
}
复制代码
然后,我们需要处理回调函数逻辑。三种状态的处理逻辑大致一致,我们就以最为复杂的pendding状态为例。
当状态为pendding时,我们需要将执行函数和拒绝函数分别推入promise的执行函数队列和拒绝函数队列中。该函数必须异步执行,并且如果执行报错,则直接执行下一个then的拒绝回调函数。还有,我们需要考虑终值可能会是一个promise对象的情况,这种情况我们封装一个函数在第三步进行实现。得到如下代码:
/**
* 回调函数处理
* @param {Function} resolveCallBack 执行回调函数
* @param {Function} rejectCallBack 拒绝回调函数
* @return {promise} 新创建的promise
*/
then (resolveCallBack, rejectCallBack) {
//如果resolveCallBack不是函数,则将值传递到下一个resolveCallBack函数中
resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;
//如果rejectCallBack不是函数,则将原因传递到下一个rejectCallBack函数中
rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
throw reason;
};
/**
* 解决promise函数
* @param {promise} promise 待解决的promise
* @param {*} x 上一个promise的终值
* @param {Function} resolve promise的执行回调函数
* @param {Function} reject promise的拒绝回调函数
*/
function resolvePromise (promise, x, resolve, reject) {
}
let pDeffer = Promise.defer();
if (this.status === 'pending') {
//如果promise状态为pending则将回调函数加入队列中
//添加解决函数队列
this.resolveFuns.push(() => {
setTimeout(() => {
try {
let x = resolveCallBack(this.value);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
});
//添加拒绝函数队列
this.rejectFuns.push(() => {
setTimeout(() => {
try {
let x = rejectCallBack(this.reason);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
});
} else if (this.status === 'resolved') {
//如果promise状态为resolved,则立即执行resolveCallBack回调函数,
//如此则无论promise是否已经执行完毕,回调函数都必然会执行
setTimeout(() => {
try {
let x = resolveCallBack(this.value);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
} else if (this.status === 'rejected') {
//如果promise状态为rejected,则立即执行rejectedCallBack回调函数,
//原因同上
setTimeout(() => {
try {
let x = rejectCallBack(this.reason);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
}
return pDeffer.promise;
}
复制代码
三、实现解决promise函数
这一步就是为了实现上述的resolvePromise
函数。
整理思路如下:
1.如果终值x与promise是同一个引用,则抛出异常(死循环)。
2.如果x是具有then函数的函数或对象,则递归调用。由于x是外部不可控变量,所以我们要保证只执行一次执行回调函数或者拒绝回调函数。
3.如果x并非具有then函数的函数或对象,则直接调用执行回调函数。
4.如果其中执行报错,则调用拒绝回调函数。
根据思路,编写出如下代码:
/**
* 解决promise函数
* @param {promise} promise 待解决的promise
* @param {*} x 上一个promise的终值
* @param {Function} resolve promise的执行回调函数
* @param {Function} reject promise的拒绝回调函数
*/
function resolvePromise (promise, x, resolve, reject) {
if (x === promise) {
reject(new TypeError('终值与Promise相等,陷入死循环!'));
return;
}
let bCalled = false;
if (x != null && (Promise.isFunction(x) || Promise.isObject(x))) {
try {
let then = x.then;
if (Promise.isFunction(then)) {
then.call(x, y => {
if (bCalled === true) {
return;
}
bCalled = true;
resolvePromise(promise, y, resolve, reject);
}, r => {
if (bCalled === true) {
return;
}
bCalled = true;
reject(r);
});
} else {
if (bCalled === true) {
return;
}
bCalled = true;
resolve(x);
}
} catch (ex) {
if (bCalled === true) {
return;
}
bCalled = true;
reject(ex);
}
} else {
if (bCalled === true) {
return;
}
bCalled = true;
resolve(x);
}
}
复制代码
这里有两个注意点:
1.声明一个变量接收x.then
变量,是为了避免多次访问x.then
属性导致其值在检索时发生改变。这个可能有点难以理解,我们看一下 promises-aplus-tests 中的测试用例源码就更容易理解了:
describe("`x` is an object with normal Object.prototype", function () {
var numberOfTimesThenWasRetrieved = null;
beforeEach(function () {
numberOfTimesThenWasRetrieved = 0;
});
function xFactory() {
return Object.create(Object.prototype, {
then: {
get: function () {
++numberOfTimesThenWasRetrieved;
return function thenMethodForX(onFulfilled) {
onFulfilled();
};
}
}
});
}
testPromiseResolution(xFactory, function (promise, done) {
promise.then(function () {
assert.strictEqual(numberOfTimesThenWasRetrieved, 1);
done();
});
});
});
复制代码
如上述代码,每次我们调用then属性时,都会导致numberOfTimesThenWasRetrieved
的值加1,从而发生一些不可预期的问题。
2.当x为promise对象时,手动调用x的then函数,然后将返回值作为终值传入promise中,就是为了处理代码执行顺序的问题。只有当上一then函数中的代码执行完毕,才会执行下一个then函数中的回调函数。因此,我们在使用链式调用时,上一then函数中执行回调函数中往往会返回一个promise对象,就是这个原理。
编码工作到此完毕。我们可以使用promises-aplus-tests插件跑一把看看结果:
完整代码地址:
欢迎关注我的微信公众号: