剖析 Promise 内部结构,一步一步实现一个完整的、能通过所有 Test case 的 Promise 类
本文写给有一定 Promise 使用经验的人,如果你还没有使用过 Promise,这篇文章可能不适合你,建议先 了解 Promise 的使用
Promise 标准解读
1、只有一个 then
方法,没有 catch
,race
,all
等方法,甚至没有构造函数
Promise 标准中仅指定了 Promise 对象的 then
方法的行为,其它一切我们常见的方法/函数都并没有指定,包括 catch
,race
,all
等常用方法,甚至也没有指定该如何构造出一个 Promise 对象,另外then也没有一般实现中(Q, $q 等)所支持的第三个参数,一般称 onProgress
2、then
方法返回一个新的Promise
Promise的then
方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释
promise2 = promise1.then(alert) promise2 != promise1 // true
3、不同 Promise 的实现需要可以相互调用(interoperable)
4、Promise 的初始状态为 pending,它可以由此状态转换为 fulfilled(本文为了一致把此状态叫做 resolved)或者 rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为 settle,更具体的标准见这里
一步一步实现一个 Promise
下面我们就来一步一步实现一个 Promise
构造函数
因为标准并没有指定如何构造一个 Promise 对象,所以我们同样以目前一般 Promise 实现中通用的方法来构造一个 Promise 对象,也是 ES6 原生 Promise 里所使用的方式,即:
// Promise构造函数接收一个executor函数,executor函数执行完同步或异步操作后,调用它的两个参数resolve和reject var promise = new Promise(function(resolve, reject) { /* 如果操作成功,调用resolve并传入https://github.com/xieranmaya/blog/issues/value 如果操作失败,调用reject并传入reason */ })
我们先实现构造函数的框架如下:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 executor(resolve, reject) // 执行executor并传入相应的参数 }
上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:
1、我们给 executor 函数传了两个参数:resolve 和 reject,这两个参数目前还没有定义
2、executor 有可能会出错(throw),类似下面这样,而如果 executor 出错,Promise 应该被其 throw 出的值 reject:
new Promise(function(resolve, reject) { throw 2 })
所以我们需要在构造函数里定义resolve和reject这两个函数:
function Promise(executor) { var self = this self.status = 'pending' // Promise当前的状态 self.data = undefined // Promise的值 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面 function resolve(https://github.com/xieranmaya/blog/issues/value) { // TODO } function reject(reason) { // TODO } try { // 考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,并且在出错后以catch到的值reject掉这个Promise executor(resolve, reject) // 执行executor } catch(e) { reject(e) } }
有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到我们在executor函数里是以resolve(https://github.com/xieranmaya/blog/issues/value)
,reject(reason)
的形式调用的这两个函数,而不是以resolve.call(promise, https://github.com/xieranmaya/blog/issues/value)
,reject.call(promise, reason)
这种形式调用的,所以这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是经过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。所以如果我们想把这两个函数定义在构造函数的外部,确实是可以这么写的:
function resolve() { // TODO } function reject() { // TODO } function Promise(executor) { try { executor(resolve.bind(this), reject.bind(this)) } catch(e) { reject.bind(this)(e) } }
但是众所周知,bind也会返回一个新的函数,这么一来还是相当于每个Promise对象都有一对属于自己的resolve
和reject
函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。
另外我们这里的实现并没有考虑隐藏this上的变量,这使得这个Promise的状态可以在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是无法从外部更改的。
接下来,我们实现resolve
和reject
这两个函数
function Promise(executor) { // ... function resolve(https://github.com/xieranmaya/blog/issues/value) { if (self.status === 'pending') { self.status = 'resolved' self.data = https://github.com/xieranmaya/blog/issues/value for(var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](https://github.com/xieranmaya/blog/issues/value) } } } function reject(reason) { if (self.status === 'pending') { self.status = 'rejected' self.data = reason for(var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } } // ... }
基本上就是在判断状态为pending之后把状态改为相应的值,并把对应的https://github.com/xieranmaya/blog/issues/value和reason存在self的data属性上面,之后执行相应的回调函数,逻辑很简单,这里就不多解释了。
then
方法
Promise对象有一个then方法,用来注册在这个Promise状态确定后的回调,很明显,then方法需要写在原型链上。then方法会返回一个Promise,关于这一点,Promise/A+标准并没有要求返回的这个Promise是一个新的对象,但在Promise/A标准中,明确规定了then要返回一个新的对象,目前的Promise实现中then几乎都是返回一个新的Promise(详情)对象,所以在我们的实现中,也让then返回一个新的Promise对象。
关于这一点,我认为标准中是有一点矛盾的:
标准中说,如果 promise2 = promise1.then(onResolved, onRejected)
里的 onResolved/onRejected 返回一个 Promise,则 promise2 直接取这个 Promise 的状态和值为己用,但考虑如下代码:
promise2 = promise1.then(function foo(https://github.com/xieranmaya/blog/issues/value) { return Promise.reject(3) })
此处如果foo
运行了,则promise1的状态必然已经确定且为resolved
,如果then返回了this(即promise2 === promise1
),说明promise2和promise1是同一个对象,而此时promise1/2的状态已经确定,没有办法再取Promise.reject(3)
的状态和结果为己用,因为Promise的状态确定后就不可再转换为其它状态。
另外每个Promise对象都可以在其上多次调用then方法,而每次调用then返回的Promise的状态取决于那一次调用then时传入参数的返回值,所以then不能返回this,因为then每次返回的Promise的结果都有可能不同。
下面我们来实现then方法:
// then方法接收两个参数,onResolved,onRejected,分别为Promise成功或失败后的回调 Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(v) {} onRejected = typeof onRejected === 'function' ? onRejected : function(r) {} if (self.status === 'resolved') { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { }) } if (self.status === 'pending') { return promise2 = new Promise(function(resolve, reject) { }) } }
Promise总共有三种可能的状态,我们分三个if块来处理,在里面分别都返回一个new Promise。
根据标准,我们知道,对于如下代码,promise2的值取决于then里面函数的返回值:
promise2 = promise1.then(function(https://github.com/xieranmaya/blog/issues/value) { return 4 }, function(reason) { throw new Error('sth went wrong') })
如果promise1被resolve了,promise2的将被4
resolve,如果promise1被reject了,promise2将被new Error('sth went wrong')
reject,更多复杂的情况不再详述。
所以,我们需要在then里面执行onResolved或者onRejected,并根据返回值(标准中记为x)来确定promise2的结果,并且,如果onResolved/onRejected返回的是一个Promise,promise2将直接取这个Promise的结果:
Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(https://github.com/xieranmaya/blog/issues/value) {} onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {} if (self.status === 'resolved') { // 如果promise1(此处即为this/self)的状态已经确定并且是resolved,我们调用onResolved // 因为考虑到有可能throw,所以我们将其包在try/catch块里 return promise2 = new Promise(function(resolve, reject) { try { var x = onResolved(self.data) if (x instanceof Promise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果 x.then(resolve, reject) } resolve(x) // 否则,以它的返回值做为promise2的结果 } catch (e) { reject(e) // 如果出错,以捕获到的错误做为promise2的结果 } }) } // 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数,就不再做过多解释 if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { try { var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) } if (self.status === 'pending') { // 如果当前的Promise还处于pending状态,我们并不能确定调用onResolved还是onRejected, // 只能等到Promise的状态确定后,才能确实如何处理。 // 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入promise1(此处即this/self)的回调数组里 // 逻辑本身跟第一个if块内的几乎一致,此处不做过多解释 return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(https://github.com/xieranmaya/blog/issues/value) { try { var x = onResolved(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(self.data) if (x instanceof Promise) { x.then(resolve, reject) } } catch (e) { reject(e) } }) }) } } // 为了下文方便,我们顺便实现一个catch方法 Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected) }
至此,我们基本实现了Promise标准中所涉及到的内容,但还有几个问题:
- 不同的Promise实现之间需要无缝的可交互,即Q的Promise,ES6的Promise,和我们实现的Promise之间以及其它的Promise实现,应该并且是有必要无缝相互调用的,比如:
// 此处用MyPromise来代表我们实现的Promise new MyPromise(function(resolve, reject) { // 我们实现的Promise setTimeout(function() { resolve(42) }, 2000) }).then(function() { return new Promise.reject(2) // ES6的Promise }).then(function() { return Q.all([ // Q的Promise new MyPromise(resolve=>resolve(8)), // 我们实现的Promise new Promise.resolve(9), // ES6的Promise Q.resolve(9) // Q的Promise ]) })
我们前面实现的代码并没有处理这样的逻辑,我们只判断了onResolved/onRejected的返回值是否为我们实现的Promise的实例,并没有做任何其它的判断,所以上面这样的代码目前是没有办法在我们的Promise里正确运行的。
- 下面这样的代码目前也是没办法处理的:
new Promise(resolve=>resolve(8)) .then() .then() .then(function foo(https://github.com/xieranmaya/blog/issues/value) { alert(https://github.com/xieranmaya/blog/issues/value) })
正确的行为应该是alert出8,而如果拿我们的Promise,运行上述代码,将会alert出undefined。这种行为称为穿透,即8这个值会穿透两个then(说Promise更为准确)到达最后一个then里的foo函数里,成为它的实参,最终将会alert出8。
下面我们首先处理简单的情况,值的穿透
Promise值的穿透
通过观察,会发现我们希望下面这段代码
new Promise(resolve=>resolve(8)) .then() .catch() .then(function(https://github.com/xieranmaya/blog/issues/value) { alert(https://github.com/xieranmaya/blog/issues/value) })
跟下面这段代码的行为是一样的
new Promise(resolve=>resolve(8)) .then(function(https://github.com/xieranmaya/blog/issues/value){ return https://github.com/xieranmaya/blog/issues/value }) .catch(function(reason){ throw reason }) .then(function(https://github.com/xieranmaya/blog/issues/value) { alert(https://github.com/xieranmaya/blog/issues/value) })
所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为function(https://github.com/xieranmaya/blog/issues/value) {return https://github.com/xieranmaya/blog/issues/value}
,function(reason) {throw reason}
。
所以我们只需要把then里判断onResolved
和onRejected
的部分改成如下即可:
onResolved = typeof onResolved === 'function' ? onResolved : function(https://github.com/xieranmaya/blog/issues/value) {return https://github.com/xieranmaya/blog/issues/value} onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}
于是Promise神奇的值的穿透也没有那么黑魔法,只不过是then默认参数就是把值往后传或者抛
不同Promise的交互
关于不同Promise间的交互,其实标准里是有说明的,其中详细指定了如何通过then的实参返回的值来决定promise2的状态,我们只需要按照标准把标准的内容转成代码即可。
这里简单解释一下标准:
即我们要把onResolved/onRejected的返回值,x,当成一个可能是Promise的对象,也即标准里所说的thenable,并以最保险的方式调用x上的then方法,如果大家都按照标准实现,那么不同的Promise之间就可以交互了。而标准为了保险起见,即使x返回了一个带有then属性但并不遵循Promise标准的对象(比如说这个x把它then里的两个参数都调用了,同步或者异步调用(PS,原则上then的两个参数需要异步调用,下文会讲到),或者是出错后又调用了它们,或者then根本不是一个函数),也能尽可能正确处理。
关于为何需要不同的Promise实现能够相互交互,我想原因应该是显然的,Promise并不是JS一早就有的标准,不同第三方的实现之间是并不相互知晓的,如果你使用的某一个库中封装了一个Promise实现,想象一下如果它不能跟你自己使用的Promise实现交互的场景。。。
建议各位对照着标准阅读以下代码,因为标准对此说明的非常详细,所以你应该能够在任意一个Promise实现中找到类似的代码:
/* resolvePromise函数即为根据x的值来决定promise2的状态的函数 也即标准中的[Promise Resolution Procedure](https://promisesaplus.com/#point-47) x为`promise2 = promise1.then(onResolved, onRejected)`里`onResolved/onRejected`的返回值 `resolve`和`reject`实际上是`promise2`的`executor`的两个实参,因为很难挂在其它的地方,所以一并传进来。 相信各位一定可以对照标准把标准转换成代码,这里就只标出代码在标准中对应的位置,只在必要的地方做一些解释 */ function resolvePromise(promise2, x, resolve, reject) { var then var thenCalledOrThrow = false if (promise2 === x) { // 对应标准2.3.1节 return reject(new TypeError('Chaining cycle detected for promise!')) } if (x instanceof Promise) { // 对应标准2.3.2节 // 如果x的状态还没有确定,那么它是有可能被一个thenable决定最终状态和值的 // 所以这里需要做一下处理,而不能一概的以为它会被一个“正常”的值resolve if (x.status === 'pending') { x.then(function(https://github.com/xieranmaya/blog/issues/value) { resolvePromise(promise2, https://github.com/xieranmaya/blog/issues/value, resolve, reject) }, reject) } else { // 但如果这个Promise的状态已经确定了,那么它肯定有一个“正常”的值,而不是一个thenable,所以这里直接取它的状态 x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { // 2.3.3 try { // 2.3.3.1 因为x.then有可能是一个getter,这种情况下多次读取就有可能产生副作用 // 即要判断它的类型,又要调用它,这就是两次读取 then = x.then if (typeof then === 'function') { // 2.3.3.3 then.call(x, function rs(y) { // 2.3.3.3.1 if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准 thenCalledOrThrow = true return resolvePromise(promise2, y, resolve, reject) // 2.3.3.3.1 }, function rj(r) { // 2.3.3.3.2 if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准 thenCalledOrThrow = true return reject(r) }) } else { // 2.3.3.4 resolve(x) } } catch (e) { // 2.3.3.2 if (thenCalledOrThrow) return // 2.3.3.3.3 即这三处谁选执行就以谁的结果为准 thenCalledOrThrow = true return reject(e) } } else { // 2.3.4 resolve(x) } }
然后我们使用这个函数的调用替换then里几处判断x是否为Promise对象的位置即可,见下方完整代码。
最后,我们刚刚说到,原则上,promise.then(onResolved, onRejected)
里的这两相函数需要异步调用,关于这一点,标准里也有说明:
In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.
所以我们需要对我们的代码做一点变动,即在四个地方加上setTimeout(fn, 0),这点会在完整的代码中注释,请各位自行发现。
事实上,即使你不参照标准,最终你在自测试时也会发现如果then的参数不以异步的方式调用,有些情况下Promise会不按预期的方式行为,通过不断的自测,最终你必然会让then的参数异步执行,让executor函数立即执行。本人在一开始实现Promise时就没有参照标准,而是自己凭经验测试,最终发现的这个问题。
至此,我们就实现了一个的Promise,完整代码如下:
try { module.exports = Promise } catch (e) {} function Promise(executor) { var self = this self.status = 'pending' self.onResolvedCallback = [] self.onRejectedCallback = [] function resolve(https://github.com/xieranmaya/blog/issues/value) { if (https://github.com/xieranmaya/blog/issues/value instanceof Promise) { return https://github.com/xieranmaya/blog/issues/value.then(resolve, reject) } setTimeout(function() { // 异步执行所有的回调函数 if (self.status === 'pending') { self.status = 'resolved' self.data = https://github.com/xieranmaya/blog/issues/value for (var i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](https://github.com/xieranmaya/blog/issues/value) } } }) } function reject(reason) { setTimeout(function() { // 异步执行所有的回调函数 if (self.status === 'pending') { self.status = 'rejected' self.data = reason for (var i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason) } } }) } try { executor(resolve, reject) } catch (reason) { reject(reason) } } function resolvePromise(promise2, x, resolve, reject) { var then var thenCalledOrThrow = false if (promise2 === x) { return reject(new TypeError('Chaining cycle detected for promise!')) } if (x instanceof Promise) { if (x.status === 'pending') { //because x could resolved by a Promise Object x.then(function(v) { resolvePromise(promise2, v, resolve, reject) }, reject) } else { //but if it is resolved, it will never resolved by a Promise Object but a static https://github.com/xieranmaya/blog/issues/value; x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { try { then = x.then //because x.then could be a getter if (typeof then === 'function') { then.call(x, function rs(y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromise(promise2, y, resolve, reject) }, function rj(r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) }) } else { resolve(x) } } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) } } else { resolve(x) } } Promise.prototype.then = function(onResolved, onRejected) { var self = this var promise2 onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return v } onRejected = typeof onRejected === 'function' ? onRejected : function(r) { throw r } if (self.status === 'resolved') { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onResolved try { var x = onResolved(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === 'rejected') { return promise2 = new Promise(function(resolve, reject) { setTimeout(function() { // 异步执行onRejected try { var x = onRejected(self.data) resolvePromise(promise2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (self.status === 'pending') { // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义 return promise2 = new Promise(function(resolve, reject) { self.onResolvedCallback.push(function(https://github.com/xieranmaya/blog/issues/value) { try { var x = onResolved(https://github.com/xieranmaya/blog/issues/value) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) self.onRejectedCallback.push(function(reason) { try { var x = onRejected(reason) resolvePromise(promise2, x, resolve, reject) } catch (r) { reject(r) } }) }) } } Promise.prototype.catch = function(onRejected) { return this.then(null, onRejected) } Promise.deferred = Promise.defer = function() { var dfd = {} dfd.promise = new Promise(function(resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd }
测试
如何确定我们实现的Promise符合标准呢?Promise有一个配套的测试脚本,只需要我们在一个CommonJS的模块中暴露一个deferred方法(即exports.deferred方法),就可以了,代码见上述代码的最后。然后执行如下代码即可执行测试:
npm i -g promises-aplus-tests promises-aplus-tests Promise.js
关于Promise的其它问题
Promise的性能问题
可能各位看官会觉得奇怪,Promise能有什么性能问题呢?并没有大量的计算啊,几乎都是处理逻辑的代码。
理论上说,不能叫做“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚我们说需要把4块代码包在setTimeout里吧,先考虑如下代码:
var start = +new Date() function foo() { setTimeout(function() { console.log('setTimeout') if((+new Date) - start < 1000) { foo() } }) } foo()
运行上面的代码,会打印出多少次'setTimeout'呢,各位可以自己试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次setTimeout运行的时间间隔约是4ms(另外,setInterval也是一样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位可以自行查阅。另外,在Node中,这个时间间隔跟浏览器不一样,经过我的测试,是1ms。
单单一个4ms的延迟可能在一般的web应用中并不会有什么问题,但是考虑极端情况,我们有20个Promise链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后一行代码的运行很可能会超过100ms,如果这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会造成一定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,但是在Node应用中,确实是有可能出现这样的case的,所以一个能够应用于生产环境的实现有必要把这个延迟消除掉。在Node中,我们可以调用process.nextTick或者setImmediate(Q就是这么做的),在浏览器中具体如何做,已经超出了本文的讨论范围,总的来说,就是我们需要实现一个函数,行为跟setTimeout一样,但它需要异步且尽早的调用所有已经加入队列的函数,这里有一个实现。
如何停止一个Promise链?
在一些场景下,我们可能会遇到一个较长的Promise链式调用,在某一步中出现的错误让我们完全没有必要去运行链式调用后面所有的代码,类似下面这样(此处略去了then/catch里的函数):
new Promise(function(resolve, reject) { resolve(42) }) .then(function(https://github.com/xieranmaya/blog/issues/value) { // "Big ERROR!!!" }) .catch() .then() .then() .catch() .then()
假设这个Big ERROR!!!
的出现让我们完全没有必要运行后面所有的代码了,但链式调用的后面即有catch,也有then,无论我们是return
还是throw
,都不可避免的会进入某一个catch
或then
里面,那有没有办法让这个链式调用在Big ERROR!!!
的后面就停掉,完全不去执行链式调用后面所有回调函数呢?
一开始遇到这个问题的时候我也百思不得其解,在网上搜遍了也没有结果,有人说可以在每个catch里面判断Error的类型,如果自己处理不了就接着throw,也有些其它办法,但总是要对现有代码进行一些改动并且所有的地方都要遵循这些约定,甚是麻烦。
然而当我从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR
后return一个Promise,但这个Promise的executor函数什么也不做,这就意味着这个Promise将永远处于pending
状态,由于then返回的Promise会直接取这个永远处于pending
状态的Promise的状态,于是返回的这个Promise也将一直处于pending
状态,后面的代码也就一直不会执行了,具体代码如下:
new Promise(function(resolve, reject) { resolve(42) }) .then(function(https://github.com/xieranmaya/blog/issues/value) { // "Big ERROR!!!" return new Promise(function(){}) }) .catch() .then() .then() .catch() .then()
这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的所有回调函数都无法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完所有回调后删除对所有回调函数的引用以让它们能被回收,在前文的实现里,为了减少复杂度,并没有做这种处理),但如果我们不使用匿名函数,而是使用函数定义或者函数变量的话,在需要多次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是可以接受的。
我们可以将返回一个什么也不做的Promise封装成一个有语义的函数,以增加代码的可读性:
Promise.cancel = Promise.stop = function() { return new Promise(function(){}) }
然后我们就可以这么使用了:
new Promise(function(resolve, reject) { resolve(42) }) .then(function(https://github.com/xieranmaya/blog/issues/value) { // "Big ERROR!!!" return Promise.stop() }) .catch() .then() .then() .catch() .then()
看起来是不是有语义的多?
Promise链上返回的最后一个Promise出错了怎么办?
考虑如下代码:
new Promise(function(resolve) { resolve(42) }) .then(function(https://github.com/xieranmaya/blog/issues/value) { alter(https://github.com/xieranmaya/blog/issues/value) })
乍一看好像没什么问题,但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert
被打成了alter
,那为什么控制台也没有报错呢,因为alter
所在的函数是被包在try/catch
块里的,alter
这个变量找不到就直接抛错了,这个错就正好成了then返回的Promise的rejection reason。
也就是说,在Promise链的最后一个then里出现的错误,非常难以发现,有文章指出,可以在所有的Promise链的最后都加上一个catch,这样出错后就能被捕获到,这种方法确实是可行的,但是首先在每个地方都加上几乎相同的代码,违背了DRY原则,其次也相当的繁琐。另外,最后一个catch依然返回一个Promise,除非你能保证这个catch里的函数不再出错,否则问题依然存在。在Q中有一个方法叫done,把这个方法链到Promise链的最后,它就能够捕获前面未处理的错误,这其实跟在每个链后面加上catch没有太大的区别,只是由框架来做了这件事,相当于它提供了一个不会出错的catch链,我们可以这么实现done方法:
Promise.prototype.done = function(){ return this.catch(function(e) { // 此处一定要确保这个函数不能再出错 console.error(e) }) }
可是,能不能在不加catch或者done的情况下,也能够让开发者发现Promise链最后的错误呢?答案依然是肯定的。
我们可以在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。以下为具体实现:
function reject(reason) { setTimeout(function() { if (self.status === 'pending') { self.status = 'rejected' self.data = reason if (self.onRejectedCallback.length === 0) { console.error(reason) } for (var i = 0; i < self.rejectedFn.length; i++) { self.rejectedFn[i](reason) } } }) }
上面的代码对于以下的Promise链也能处理的很好:
new Promise(function(){ // promise1 reject(3) }) .then() // returns promise2 .then() // returns promise3 .then() // returns promise4
看起来,promise1,2,3,4都没有处理函数,那是不是会在控制台把这个错误输出 4 次呢,并不会,实际上,promise1,2,3 都隐式的有处理函数,就是then的默认参数,各位应该还记得then的默认参数最终是被push到了Promise的callback数组里。只有 promise4 是真的没有任何 callback,因为压根就没有调用它的then方法。
事实上,Bluebird和ES6 Promise都做了类似的处理,在Promise被reject但又没有callback时,把错误输出到控制台。Q使用了done方法来达成类似的目的,$q在最新的版本中也加入了类似的功能。
Angular 里的 $q 跟其它 Promise 的交互
一般来说,我们不会在 Angular 里使用其它的 Promise,因为 Angular 已经集成了 $q,但有些时候我们在Angular里需要用到其它的库(比如LeanCloud的JS SDK),而这些库或是封装了ES6的Promise,或者是自己实现了Promise,这时如果你在Angular里使用这些库,就有可能发现视图跟Model不同步。究其原因,是因为$q已经集成了Angular的digest loop机制,在Promise被resolve或reject时触发digest,而其它的Promise显然是不会集成的,所以如果你运行下面这样的代码,视图是不会同步的:
app.controller(function($scope) { Promise.resolve(42).then(function(https://github.com/xieranmaya/blog/issues/value) { $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/value }) })
Promise 结束时并不会触发 digest,所以视图没有同步。$q 上正好有个 when 方法,它可以把其它的 Promise 转换成 $q 的 Promise(有些Promise实现中提供了Promise.cast函数,用于将一个thenable转换为它的Promise),问题就解决了:
app.controller(function($scope, $q) { $q.when(Promise.resolve(42)).then(function(https://github.com/xieranmaya/blog/issues/value) { $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/value }) })
当然也有其它的解决方案比如在其它 Promise 的链的最后加一个 digest,类似下面这样:
Promise.prototype.$digest = function() { $rootScope.$digest() return this } // 然后这么使用 OtherPromise .resolve(42) .then(function(https://github.com/xieranmaya/blog/issues/value) { $scope.https://github.com/xieranmaya/blog/issues/value = https://github.com/xieranmaya/blog/issues/value }) .$digest()
因为使用场景并不多,此处不做深入讨论。
出错时,是用 throw new Error() 还是用 return Promise.reject(new Error()) 呢?
这里我觉得主要从性能和编码的舒适度角度考虑:
性能方面,throw new Error()
会使代码进入catch
块里的逻辑(还记得我们把所有的回调都包在try/catch里了吧),传说throw
用多了会影响性能,因为一但throw
,代码就有可能跳到不可预知的位置。
但考虑到onResolved/onRejected
函数是直接被包在Promise实现里的try
里,出错后就直接进入了这个try
对应 的catch
块,代码的跳跃“幅度”相对较小,我认为这里的性能损失可以忽略不记。有机会可以测试一下。
而使用 Promise.reject(new Error())
,则需要构造一个新的Promise对象(里面包含2个数组,4个函数:resolve/reject
,onResolved/onRejected
),也会花费一定的时间和内存。
而从编码舒适度的角度考虑,出错用 throw
,正常时用 return
,可以比较明显的区分出错与正常,throw
和return
又同为关键字,用来处理对应的情况也显得比较对称(-_-)。另外在一般的编辑器里,Promise.reject
不会被高亮成与 throw
和 return
一样的颜色。最后,如果开发者又不喜欢构造出一个Error
对象的话,Error
的高亮也没有了。
综上,我觉得在Promise里发现显式的错误后,用 throw
抛出错误会比较好,而不是显式的构造一个被reject的Promise对象。
最佳实践
这里不免再啰嗦两句最佳实践
1、一是不要把 Promise 写成嵌套结构,至于怎么改进,这里就不多说了
// 错误的写法 promise1.then(function(https://github.com/xieranmaya/blog/issues/value) { promise1.then(function(https://github.com/xieranmaya/blog/issues/value) { promise1.then(function(https://github.com/xieranmaya/blog/issues/value) { }) }) })
2、二是链式 Promise 要返回一个 Promise,而不只是构造一个 Promise
// 错误的写法 Promise.resolve(1).then(function(){ Promise.resolve(2) }).then(function(){ Promise.resolve(3) })
Promise 相关的 convenience method 的实现
请到这里查看 Promise.race
, Promise.all
, Promise.resolve
, Promise.reject
等方法的具体实现,这里就不具体解释了,总的来说,只要 then 的实现是没有问题的,其它所有的方法都可以非常方便的依赖then来实现。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 从如何停掉 Promise 链说起
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(47)
mark~~!!
测试发现 resolve 函数里面的判断是没必要的,忽略掉就好
当 promise 对象已经处非 pending 的状态时,就没法调用 resolve 或 reject 方法了呀,所以也就不存在一起执行的条件。这时 promise 的 executor 已经处理完成任务并且把运行结果挂在 https://github.com/xieranmaya/blog/issues/value 上了,那么就直接从 https://github.com/xieranmaya/blog/issues/value 里取出来然后直接运行回调
process.nextTick(()=>{})
你好,我遇到了同样的问题,但是我不是很能理解你说的放到函数顶部。我理解递归调用的时候,这个判断调用一次的flag在每次调用的时候都会被重置为false
你好,我想请问一下resolve和reject函数内部的实现为什么是需要整个函数体放到setTimeout。我理解状态的改变从pending到fulfilled或者rejected是一个同步的操作。但是实际上我尝试把状态这部分代码放到setTimeout外部之后测试用例就无法通过了。
我尝试了一下浏览器实现版的promise和通过测试用例的_Promise都运行下面这段代码:
实际上浏览器的Promise打出来的结果是
而实现版打出来的结果是
我理解是不是因为浏览器的状态改变是同步操作的结果。希望大佬能帮我解答下问题
这个纯粹是语法错误了。class 和构造函数还是有区别的,怎么可以直接在方法外面编写执行语句?运行测试的时候应该会报错 unexcepted token "="
mark
mark
mark 很不错~慢慢品尝
这里用setTimeout和原生的Promise是有区别的因为setTimeout是macro-task,而Promise是micro-task。micro-task应该优先执行于macro-task , 所以如果你上面用setTimeout 就会导致如果Promise之前有setTimeout分配的task,执行顺序就会出错。应该用process.nextTick来代替setTimeout。但是不过好像在Browser端没有对应的替代方案也只有setTimeout了。文章不错,赞一个
bluebird的实现好像是node端首选process.nextTick其次是setImmediate 。 browser端首选MutationObserver其次是setTimeout
是的,这个我文中有提到。
另外关于两种task的区别,这种情况下,如果全用setTimeout的话也是不会错乱的。在支持的环境中用更“高效”的函数就可以了
面试的时候面试官让我手写一个Promise的实现,当时实在不会,现在回来赶紧学习,学到了
@xieranmaya 照着楼主的思路自己实现了一下,有一处代码比较有疑惑,
注释中写到 because x could resolved by a Promise Object
可是在 Promise 的构造里面不是写了
这样最后 resolve 调用时候不是一定不是一个
Promise
的实例么?(我理解中第一段代码就是考虑v
可能Promise
的情况)望指点~
@zhuscat
我没有仔细看过源码,不过我想实现思路应该都差不多的..
第一段代码的意思是
如果 x 是一个 Promise,并且状态还处于 pending,那么先 then 拿到最终的 https://github.com/xieranmaya/blog/issues/value,接下来才 resolve https://github.com/xieranmaya/blog/issues/value
第二段代码应该是处理类似
这样的情况的。
@think2011 有点没理解。
第一段代码考虑的不是
x
中的resolve
可能是resolve
一个Promise
吗?但是构造函数中写了,当resolve
一个Promise
的时候,执行下面的代码mark
写的不错!很详细
非常详细,我自己实现了一小半都写不下去了,佩服作者
同问,我也没理解这一段@zhuscat
LZ有两个问题,麻烦解答一下then方法就是负责执行resovle和reject 通过onResovle和onReject已经执行了, 为啥在在"resolve"状态下后面还执行一次resolve 在reject和pengding状态都没有执行 。 还有
只有resolve才有这个判断,reject没有
是不是应该在三种状态下执行try成功后,都应该执行resolve函数呢 以确保后面都链式操作继续执行
@xieranmaya
若果x里面还有一层或几层promise嵌套,你这样就不行了
我有一个promise关于microtask的问题:
我看到resolve,reject都是用setTimout实现的,而setTimout是macrotask。
那么下面这种情况:
setTimeout(() => {
console.log('setTimeout')
})
const p1 = new MyPromise(r =>{
console.log(1)
r()
}).then(v => {
console.log(2)
})
如果resolve使用setTimeout实现的,那么打印顺序就应该是 1, setTimeout, 2
第一个宏任务script执行完打印了1,并且把遇到的setTimeout与在resolve中执行的console.log(2),都归于下一个宏任务和下下一个宏任务,就会依次打印setTimeout和2.
而promise是归于微任务,第一个宏任务完了会立刻执行被推入队列的微任务
也就是会打印 1 2 setTimout.
所以想问一下,promsie是如何在浏览器中实现这种走微任务队列的呢?
@jiaweiCao @ym754870370 @Arsenal072
统一回复一些问题
首先关于使用setTimeout,这个实现只负责把测试用例全过,并不考虑现实场景,而A+标准中并没有对回调的调用方式进行限制,只要求回调函数在promise自身的调用栈以外调用(请仔细理解这句话),所有的测试用例也只考虑promise自身的调度,不考虑与其它异步函数(如nextTick,setImmediate等)的协同使用。任何语言只要提供了类似setTimeout的功能都是可以实现promise的,显然其它语言不见得有js中宏任务与微任务的区分。所以测试用例不会区分回调是用哪种任务类型调用的。
实际的实现中都是用微任务执行的,但考虑到高版本浏览器自带Promise,这个实现如果运行,也只需要在低版本中,那么是没有微任务函数的。另外这样写也是为了一些新手容易看懂。
另外,A+标准与ES6 Promise的标准是有不同的,ES6 Promise的标准严格很多。
然后中间的那段条件判断,并不是什么刻意写的,是可以优化为更好的逻辑的,只是当时那么写确实通过了所有的test case,然后写文章的时候就直接贴过来了,我又不想修改文章的最后更新时间,就一直留着了,理解好标准完全可以改成你们喜欢的样子,只要逻辑是符合标准定义的就行。
@JunlinZhu-Tommy
then肯定是不能返回this的:
很显然a和b一个成功一个失败,如果返回this,a和b岂不是都为p,都成功?
但是在一些情况下是可以返回this的,在A+标准的3.3有明确描述,只要你的实现满足的所有的规则,比如说以下的情况就可以直接返回this:
显然,a和p的最终resolve的值是一样的。
也即是在调用p的then时p的状态已经确定为某种状态,但又没给then传相应的参数时。
不能这样写,按你的写法,onResolved会在调用then的时候同步调用,但回调必须异步调用。
这么久了我也不确定细节,你改了可以跑一下测试,能通过就可以,不能通过就拿用例跑一下就明白了。
@Arsenal072 这个回答说的很对。
resolve一般是被executor异步调用的,调用时,对象上已经有了回调函数了。
即使是被同步调用也没关系,同步调用时状态已经改变,再调用then时有了状态就会立刻运行传入的回调,而不push到数组里。
我也觉得这边要么加else,要么加return;同理self.status === 'rejected'时也应该加上,我觉得时作者漏了。这边逻辑其实和 function resolve()中是一样的
我也有这个疑问,我猜测是,then 里返回的是一个 新 promise 对象 p2, 那么肯定需要 p2 调用了 resovle 函数,后面链式的 then 里的函数才能执行。 如果直接 resolve(x), 那么相当于把 x 这个promsie 对象传递给了 then 参数,这肯定不符合预期。 个人理解写成
var x = onResolved(self.data); if ( x instanceof Promise) { x.then(data => resolve(data)); } else { resolve(x); }
可能更好理解一些。
似乎不能使用class实现Promise,我照着大佬的代码写了一遍class版本的,并不能通过测试脚本,我找了很久的问题,也没有找到。
我的理解是,红框部分应该是有三个promise的,第一个是调用then时里面新创建的promise,也就是promise2,第二个是用户自己创建的promise,也就是x,最后一个是判断x为promise后,调用x的then方法里面又新建的promise,称为thenPromise,也就是x.then(resolve, reject)时内部创建的promise,如此一来,当then内部判断x为promise时,就执行x.then(resolve, reject),此时promise2的resolve作为参数传入then中被当作onResolved方法执行,这样便把用户自己创建的promise传入的值传递给了promise2,以便后面的then继续使用,但是因为resolve本身没有返回值,所以此时的thenPromise中的x的值为undefined,不过此时并不影响后续的操作了,因为promise2已经拿到了用户新建promise时要传递的值了
当p resolve(1)时,p.then()返回的新的promise2,默认有onFulfilled函数,执行,也就是图中的x = onFulfilled(https://github.com/xieranmaya/blog/issues/value),要用promise2的resolve来决议promise2吧,这样p.then()返回的promise才是决议掉的(resolved)。然后p.then().then(),第二个then返回的promise会用前一个then()返回的promise的终值作为自己的终值,不然直接被第一个then()返回的promise卡着了,如果不决议它的话。
这里的写法我用的else { resolve(x) }。而作者在
function resolve
的实现中是if(){...}; resolve(x)
,我觉得有点问题,分析如下大佬,有个问题请教下。
onResolvedCallback
为什么是一个数组队列呢?我试了好久,也只弄出一个回调函数,什么情况下会出现多个回调。resolvePromise
方法里这一段能不能直接去掉呢,因为promise也是thenable,我测了也能通过@HeskeyBaozi 这个问题在我最初实现的时候也是纠结了我好久,最终跟你差不多,发现还是自己没有考虑全面~~我的代码里面也有几乎相同的逻辑,你在当前页面搜“thenCalledOrThrow”就能看到了~
我根据大大的教程实现了一遍。但是在测试框架的时候一直提示我resolvePromise这个参数函数的y参数处理有问题= =。
测试报告:
2.3.3.3.1: If/when
resolvePromise
is called with https://github.com/xieranmaya/blog/issues/valuey
, run[[Resolve]](promise, y)
y
is a thenable for a thenabley
is an already-fulfilled promise for a synchronously-fulfilled custom thenablethen
callsresolvePromise
synchronouslyvia return from a fulfilled promise
Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
via return from a rejected promise
Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
then
callsresolvePromise
asynchronouslyvia return from a fulfilled promise
Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
via return from a rejected promise
Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
下面是resolveProomise实现:
ww我找到原因了。
在判断thenable对象的时候,resolvePromise和rejectPromise应该只调用一次,尤其是在Promise对象和thenable交错嵌套的时候。
@nobodiness 问题提的很好,不过注意看源代码, try块是包在setTimeout里面的
这里
var x = onResolved(self.data)
直接把值算出来了, 异步不就变成同步了么, 计算onResolved
一定会卡住的THX
受教了,谢谢分享
@xcatliu typo,已改~
应该是「违背了DRY原则」?
Great.
来不及全部看完,很好奇! 回头接着看。