本文写给有一定Promi使用经验的人,如果你还没有使用过Promi,这篇文章可能不适合你,建议先了解Promi的使用
Promi标准解读
1.只有一个then方法,没有catch,race,all等方法,甚至没有构造函数
Promi标准中仅指定了Promi对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catch,race,all等常用方法,甚至也没有指定该如何构造出一个Promi对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgress
2.then方法返回一个新的Promi
Promi的then方法返回一个新的Promi,而不是返回this,此处在下文会有更多解释
promi2 = promi1.then(alert)promi2 != promi1 // true
3.不同Promi的实现需要可以相互调用(interoperable)
4.Promi的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为ttle
5.更具体的标准见这里
一步一步实现一个Promi
下面我们就来一步一步实现一个Promi
构造函数
因为标准并没有指定如何构造一个Promi对象,所以我们同样以目前一般Promi实现中通用的方法来构造一个Promi对象,也是ES6原生Promi里所使用的方式,即:
// Promi构造函数接收一个executor函数,executor函数执行完同步或异步操作后,调用它的两个参数resolve和rejectvar promi = new Promi(function(resolve, reject) { /* 如果操作成功,调用resolve并传入value 如果操作失败,调用reject并传入reason */})
我们先实现构造函数的框架如下:
function Promi(executor) { var lf = this lf.status = 'pending' // Promi当前的状态 lf.data = undefined // Promi的值 lf.onResolvedCallback = [] // Promi resolve时的回调函数集,因为在Promi结束之前有可能有多个回调添加到它上面 lf.onRejectedCallback = [] // Promi reject时的回调函数集,因为在Promi结束之前有可能有多个回调添加到它上面 executor(resolve, reject) // 执行executor并传入相应的参数}
上面的代码基本实现了Promi构造函数的主体,但目前还有两个问题:
1.我们给executor函数传了两个参数:resolve和reject,这两个参数目前还没有定义
2.executor有可能会出错(throw),类似下面这样,而如果executor出错,Promi应该被其throw出的值reject:
new Promi(function(resolve, reject) { throw 2})
所以我们需要在构造函数里定义resolve和reject这两个函数:
function Promi(executor) { var lf = this lf.status = 'pending' // Promi当前的状态 lf.data = undefined // Promi的值 lf.onResolvedCallback = [] // Promi resolve时的回调函数集,因为在Promi结束之前有可能有多个回调添加到它上面 lf.onRejectedCallback = [] // Promi reject时的回调函数集,因为在Promi结束之前有可能有多个回调添加到它上面 function resolve(value) { // TODO } function reject(reason) { // TODO } try { // 考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,并且在出错后以catch到的值reject掉这个Promi executor(resolve, reject) // 执行executor } catch(e) { reject(e) }}
有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到我们在executor函数里是以resolve(value),reject(reason)的形式调用的这两个函数,而不是以resolve.call(promi, value),reject.call(promi, reason)这种形式调用的,所以这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是经过bind后传给了executor,要么它们定义在构造函数的内部,使用lf来访问所属的Promi对象。所以如果我们想把这两个函数定义在构造函数的外部,确实是可以这么写的:
function resolve() { // TODO}function reject() { // TODO}function Promi(executor) { try { executor(resolve.bind(this), reject.bind(this)) } catch(e) { reject.bind(this)(e) }}
但是众所周知,bind也会返回一个新的函数,这么一来还是相当于每个Promi对象都有一对属于自己的resolve和reject函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。
另外我们这里的实现并没有考虑隐藏this上的变量,这使得这个Promi的状态可以在executor函数外部被改变,在一个靠谱的实现里,构造出的Promi对象的状态和最终结果应当是无法从外部更改的。
接下来,我们实现resolve和reject这两个函数
function Promi(executor) { // ... function resolve(value) { if (lf.status === 'pending') { lf.status = 'resolved' lf.data = value for(var i = 0; i < lf.onResolvedCallback.length; i++) { lf.onResolvedCallback[i](value) } } } function reject(reason) { if (lf.status === 'pending') { lf.status = 'rejected' lf.data = reason for(var i = 0; i < lf.onRejectedCallback.length; i++) { lf.onRejectedCallback[i](reason) } } } // ...}
基本上就是在判断状态为pending之后把状态改为相应的值,并把对应的value和reason存在lf的data属性上面,之后执行相应的回调函数,逻辑很简单,这里就不多解释了。
then方法
Promi对象有一个then方法,用来注册在这个Promi状态信息管理专业确定后的回调,很明显,then方法需要写在原型链上。then方法会返回一个Promi,关于这一点,Promi/A+标准并没有要求返回的这个Promi是一个新的对象,但在Promi/A标准中,明确规定了then要返回一个新的对象,目前的Promi实现中then几乎都是返回一个新的Promi(详情)对象,所以在我们的实现中,也让then返回一个新的Promi对象。
关于这一点,我认为标准中是有一点矛盾的:
标准中说,如果promi2 = promi1.then(onResolved, onRejected)里的onResolved/onRejected返回一个Promi,则promi2直接取这个Promi的状态和值为己用,但考虑如下代码:
promi2 = promi1.then(function foo(value) { return Promi.reject(3)})
此处如果foo运行了,则promi1的状态必然已经确定且为resolved,如果then返回了this(即promi2 === promi1),说明promi2和promi1是同一个对象,而此时promi1/2的状态已经确定,没有办法再取Promi.reject(3)的状态和结果为己用,因为Promi的状态确定后就不可再转换为其它状态。
另外每个Promi对象都可以在其上多次调用then方法,而每次调用then返回的Promi的状态取决于那一次调用then时传入参数的返回值,所以then不能返回this,因为then每次返回的Promi的结果都有可能不同。
下面我们来实现then方法:
// then方法接收两个参数,onResolved,onRejected,分别为Promi成功或失败后的回调Promi.prototype.then = function(onResolved, onRejected) { var lf = this var promi2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(v) {} onRejected = typeof onRejected === 'function' ? onRejected : function(r) {} if (lf.status === 'resolved') { return promi2 = new Promi(function(resolve, reject) { }) } if (lf.status === 'rejected') { return promi2 = new Promi(function(resolve, reject) { }) } if (lf.status === 'pending') { return promi2 = new Promi(function(resolve, reject) { }) }}
Promi总共有三种可能的状态,我们分三个if块来处理,在里面分别都返回一个new Promi。
根据标准,我们知道,对于如下代码,promi2的值取决于then里面函数的返回值:
promi2 = promi1.then(function(value) { return 4}, function(reason) { throw new Error('sth went wrong')})
如果promi1被resolve了,promi2的将被4 resolve,如果promi1被reject了,promi2将被new Error(‘sth went wrong’) reject,更多复杂的情况不再详述。
所以,我们需要在then里面执行onResolved或者onRejected,并根据返回值(标准中记为x)来确定promi2的结果,并且,如果onResolved/onRejected返回的是一个Promi,promi2将直接取这个Promi的结果:
Promi.prototype.then = function(onResolved, onRejected) { var lf = this var promi2 // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理 onResolved = typeof onResolved === 'function' ? onResolved : function(value) {} onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {} if (lf.status === 'resolved') { // 如果promi1(此处即为this/lf)的状态已经确定并且是resolved,我们调用onResolved // 因为考虑到有可能throw,所以我们将其包在try/catch块里 return promi2 = new Promi(function(resolve, reject) { try { var x = onResolved(lf.data) if (x instanceof Promi) { // 如果onResolved的返回值是一个Promi对象,直接取它的结果做为promi2的结果 x.then(resolve, reject) } resolve(x) // 否则,以它的返回值做为promi2的结果 } catch (e) { reject(e) // 如果出错,以捕获到的错误做为promi2的结果 } }) } // 此处与前一个if块的逻辑几乎相同,区别在于所调用的是onRejected函数,就不再做过多解释 if (lf.status === 'rejected') { return promi2 = new Promi(function(resolve, reject) { try { var x = onRejected(lf.data) if (x instanceof Promi) { x.then(resolve, reject) } } catch (e) { reject(e) } }) } if (lf.status === 'pending') { // 如果当前的Promi还处于pending状态,我们并不能确定调用onResolved还是onRejected, // 只能等到Promi的状态确定后,才能确实如何处理。 // 所以我们需要把我们的**两种情况**的处理逻辑做为callback放入promi1(此处即this/lf)的回调数组里 // 逻辑本身跟第一个if块内的几乎一致,此处不做过多解释 return promi2 = new Promi(function(resolve, reject) { lf.onResolvedCallback.push(function(value) { try { var x = onResolved(lf.data) if (x instanceof Promi) { x.then(resolve, reject) } } catch (e) { reject(e) } }) lf.onRejectedCallback.push(function(reason) { try { var x = onRejected(lf.data) if (x instanceof Promi) { x.then(resolve, reject) } } catch (e) { reject(e) } }) }) }}// 为了下文方便,我们顺便实现一个catch方法Promi.prototype.catch = function(onRejected) { return this.then(null, onRejected)}
至此,我们基本实现了Promi标准中所涉及到的内容,但还有几个问题:
1.不同的Promi实现之间需要无缝的可交互,即Q的Promi,ES6的Promi,和我们实现的Promi之间以及其它的Promi实现,应该并且是有必要无缝相互调用的,比如:
// 此处用MyPromi来代表我们实现的Prominew MyPromi(function(resolve, reject) { // 我们实现的Promi tTimeout(function() { resolve(42) }, 2000)}).then(function() { return new Promi.reject(2) // ES6的Promi}).then(function() { return Q.all([ // Q的Promi new MyPromi(resolve=>resolve(8)), // 我们实现的Promi new Promi.resolve(9), // ES6的Promi Q.resolve(9) // Q的Promi ])})
我们前面实现的代码并没有处理这样的逻辑,我们只判断了onResolved/onRejected的返回值是否为我们实现的Promi的实例,并没有做任何其它的判断,所以上面这样的代码目前是没有办法在我们的Promi里正确运行的。
2.下面这样的代码目前也是没办法处理的:
new Promi(resolve=>resolve(8)) .then() .then() .then(function foo(value) { alert(value) })
正确的行为应该是alert出8,而如果拿我们的Promi,运行上述代码,将会alert出undefined。这种行为称为穿透,即8这个值会穿透两个then(说Promi更为准确)到达最后一个then里的foo函数里,成为它的实参,最终将会alert出8。
下面我们首先处理简单的情况,值的穿透
Promi值的穿透
通过观察,会发现我们希望下面这段代码
new Promi(resolve=>resolve(8)) .then() .catch() .then(function(value) { alert(value) })
跟下面这段代码的行为是一样的
new Promi(resolve=>resolve(8)) .then(function(value){ return value }) .catch(function(reason){ throw reason }) .then(function(value) { alert(value) })
所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为function(value) {return value},function(reason) {throw reason}。
所以我们只需要把then里判断onResolved和onRejected的部分改成如下即可:
onResolved = typeof onResolved === 'function' ? onResolved : function(value) {return value}onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {throw reason}
于是Promi神奇的值的穿透也没有那么黑魔法,只不过是then默认参数就是把值往后传或者抛
不同Promi的交互
关于不同Promi间的交互,其实标准里是有说明的,其中详细指定了如何通过then的实参返回的值来决定promi2的状态,我们只需要按照标准把标准的内容转成代码即可。
这里简单解释一下标准:
即我们要把onResolved/onRejected的返回值,x,当成一个可能是Promi的对象,也即标准里所说的thenable,并以最保险的方式调用x上的then方法,如果大家都按照标准实现,那么不同的Promi之间就可以交互了。而标准为了保险起见,即使x返回了一个带有then属性但并不遵循Promi标准的对象(比如说这个x把它then里的两个参数都调用了,同步或者异步调用(PS,原则上then的两个参数需要异步调用,下文会讲到),或者是出错后又调用了它们,或者then根本不是一个函数),也能尽可能正确处理。
关于为何需要不同的Promi实现能够相互交互,我想原因应该是显然的,Promi并不是JS一早就有的标准,不同第三方的实现之间是并不相互知晓的,如果你使用的某一个库中封装了一个Promi实现,想象一下如果它不能跟你自己使用的Promi实现交互的场景。。。
建议各位对照着标准阅读以下代码,因为标准对此说明的非常详细,所以你应该能够在任意一个Promi实现中找到类似的代码:
/*resolvePromi函数即为根据x的值来决定promi2的状态的函数也即标准中的[Promi Resolution Procedure](/d/file/titlepic/ = promi1.then(onResolved, onRejected)`里`onResolved/onRejected`的返回值`resolve`和`reject`实际上是`promi2`的`executor`的两个实参,因为很难挂在其它的地方,所以一并传进来。相信各位一定可以对照标准把标准转换成代码,这里就只标出代码在标准中对应的位置,只在必要的地方做一些解释*/function resolvePromi(promi2, x, resolve, reject) { var then var thenCalledOrThrow = fal if (promi2 === x) { // 对应标准2.3.1节 return reject(new TypeError('Chaining cycle detected for promi!')) } if (x instanceof Promi) { // 对应标准2.3.2节 // 如果x的状态还没有确定,那么它是有可能被一个thenable决定最终状态和值的 // 所以这里需要做一下处理,而不能一概的以为它会被一个“正常”的值resolve if (x.status === 'pending') { x.then(function(value) { resolvePromi(promi2, value, resolve, reject) }, reject) } el { // 但如果这个Promi的状态已经确定了,那么它肯定有一个“正常”的值,而不是一个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 resolvePromi(promi2, 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) }) } el { // 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) } } el { // 2.3.4 resolve(x) }}
然后我们使用这个函数的调用替换then里几处判断x是否为Promi对象的位置即可,见下方完整代码。
最后,我们刚刚说到,原则上,promi.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.
所以我们需要对我们的代码做一点变动,即在四个地方加上tTimeout(fn, 0),这点会在完整的代码中注释,请各位自行发现。
事实上,即使你不参照标准,最终你在自测试时也会发现如果then的参数不以异步的方式调用,有些情况下Promi会不按预期的方式行为,通过不断的自测,最终你必然会让then的参数异步执行,让executor函数立即执行。本人在一开始实现Promi时就没有参照标准,而是自己凭经验测试,最终发现的这个问题。
至此,我们就实现了一个的Promi,完整代码如下:
try { module.exports = Promi} catch (e) {}function Promi(executor) { var lf = this lf.status = 'pending' lf.onResolvedCallback = [] lf.onRejectedCallback = [] function resolve(value) { if (value instanceof Promi) { return value.then(resolve, reject) } tTimeout(function() { // 异步执行所有的回调函数 if (lf.status === 'pending') { lf.status = 'resolved' lf.data = value for (var i = 0; i < lf.onResolvedCall红色故事back.length; i++) { lf.onResolvedCallback[i](value) } } }) } function reject(reason) { tTimeout(function() { // 异步执行所有的回调函数 if (lf.status === 'pending') { lf.status = 'rejected' lf.data = reason for (var i = 0; i < lf.onRejectedCallback.length; i++) { lf.onRejectedCallback[i](reason) } } }) } try { executor(resolve, reject) } catch (reason) { reject(reason) }}function resolvePromi(promi2, x, resolve, reject) { var then var thenCalledOrThrow = fal if (promi2 === x) { return reject(new TypeError('Chaining cycle detected for promi!')) } if (x instanceof Promi) { if (x.status === 'pending') { //becau x could resolved by a Promi Object x.then(function(v) { resolvePromi(promi2, v, resolve, reject) }, reject) } el { //but if it is resolved, it will never resolved by a Promi Object but a static value; x.then(resolve, reject) } return } if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) { try { then = x.then //becau x.then could be a getter if (typeof then === 'function') { then.call(x, function rs(y) { if (thenCalledOrThrow) return thenCalledOrThrow = true return resolvePromi(promi2, y, resolve, reject) }, function rj(r) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(r) }) } el { resolve(x) } } catch (e) { if (thenCalledOrThrow) return thenCalledOrThrow = true return reject(e) } } el { resolve(x) }}Promi.prototype.then = function(onResolved, onRejected) { var lf = this var promi2 onResolved = typeof onResolved === 'function' ? onResolved : function(v) { return v } onRejected = typeof onRejected === 'function' ? onRejected : function(r) { throw r } if (lf.status === 'resolved') { return promi2 = new Promi(function(resolve, reject) { tTimeout(function() { // 异步执行onResolved try { var x = onResolved(lf.data) resolvePromi(promi2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (lf.status === 'rejected') { return promi2 = new Promi(function(resolve, reject) { tTimeout(function() { // 异步执行onRejected try { var x = onRejected(lf.data) resolvePromi(promi2, x, resolve, reject) } catch (reason) { reject(reason) } }) }) } if (lf.status === 'pending') { // 这里之所以没有异步执行,是因为这些函数必然会被resolve或reject调用,而resolve或reject函数里的内容已是异步执行,构造函数里的定义 return promi2 = new Promi(function(resolve, reject) { lf.onResolvedCallback.push(function(value) { try { var x = onResolved(value) resolvePromi(promi2, x, resolve, reject) } catch (r) { reject(r) } }) lf.onRejectedCallback.push(function(reason) { try { var x = onRejected(reason) resolvePromi(promi2, x, resolve, reject) } catch (r) { reject(r) } }) }) }}Promi.prototype.catch = function(onRejected) { return this.then(null, onRejected)}Promi.deferred = Promi.defer = function() { var dfd = {} dfd.promi = new Promi(function(resolve, reject) { dfd.resolve = resolve dfd.reject = reject }) return dfd}
测试
如何确定我们实现的Promi符合标准呢?Promi有一个配套的测试脚本,只需要我们在一个CommonJS的模块中暴露一个deferred方法(即exports.deferred方法),就可以了,代码见上述代码的最后。然后执行如下代码即可执行测试:
npm i -g promis-aplus-testspromis-aplus-tests Promi.js
关于Promi的其它问题 Promi的性能问题
可能各位看官会觉得奇怪,Promi能有什么性能问题呢?并没有大量的计算啊,几乎都是处理逻辑的代码。
理论上说,不能叫做“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚我们说需要把4块代码包在tTimeout里吧,先考虑如下代码:
var start = +new Date()function foo() { tTimeout(function() { console.log('tTimeout') if((+new Date) - start < 1000) { foo() } })}foo()
运行上面的代码,会打印出多少次’tTimeout’呢,各位可以自己试一下,不出意外的话,应该是250次左右,我刚刚运行了一次,是241次。这说明,上述代码中两次tTimeout运行的时间间隔约是4ms(另外,tInterval也是一样的),实事上,这正是浏览器两次Event Loop之间的时间间隔,相关标准各位可以自行查阅。另外,在Node中,这个时间间隔跟浏览器不一样,经过我的测试,是1ms。
单单一个4ms的延迟可能在一般的web应用中并不会有什么问题,但是考虑极端情况,我们有20个Promi链式调用,加上代码运行的时间,那么这个链式调用的第一行代码跟最后家长给老师拜年祝福语一行代码的运行很可能会超过100ms,如果这之间没有对UI有任何更新的话,虽然本质上没有什么性能问题,但可能会造成一定的卡顿或者闪烁,虽然在web应用中这种情形并不常见,但是在Node应用中,确实是有可能出现这样的ca的,所以一个能够应用于生产环境的实现有必要把这个延迟消除掉。在Node中,我们可以调用process.nextTick或者tImmediate(Q就是这么做的),在浏览器中具体如何做,已经超出了本文的讨论范围,总的来说,就是我们需要实现一个函数,行为跟tTimeout一样,但它需要异步且尽早的调用所有已经加入队列的函数,这里有一个实现。
如何停止一个Promi链?
在一些场景下,我们可能会遇到一个较长的Promi链式调用,在某一步中出现的错误让我们完全没有必要去运行链式调用后面所有的代码,类似下面这样(此处略去了then/catch里的函数):
new Promi(function(resolve, reject) { resolve(42)}) .then(function(value) { // "Big ERROR!!!" }) .catch() .then() .then() .catch() .then()
假设这个Big ERROR!!!的出现让我们完全没有必要运行后面所有的代码了,但链式调用的后面即有catch,也有then,无论我们是return还是throw,都不可避免的会进入某一个catch或then里面,那有没有办法让这个链式调用在Big ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?
一开始遇到这个问题的时候我也百思不得其解,在网上搜遍了也没有结果,有人说可以在每个catch里面判断Error的类型,如果自己处理不了就接着throw,也有些其它办法,但总是要对现有代码进行一些改动并且所有的地方都要遵循这些约定,甚是麻烦。
然而当我从一个实现者的角度看问题时,确实找到了答案,就是在发生Big ERROR后return一个Promi,但这个Promi的executor函数什么也不做,这就意味着这个Promi将永远处于pending状态,由于then返回的Promi会直接取这个永远处于pending状态的Promi的状态,于是返回的这个Promi也将一直处于pending状态,后面的代码也就一直不会执行了,具体代码如下:
new Promi(function(resolve, reject) { resolve(42)}) .then(function(value) { // "Big ERROR!!!" return new Promi(function(){}) }) .catch() .then() .then() .catch() .then()
这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的所有回调函数都无法被垃圾回收器回收(在一个靠谱的实现里,Promi应该在执行完所有回调后删除对所有回调函数的引用以让它们能被回收,在前文的实现里,为了减少复杂度,并没有做这种处理),但如果我们不使用匿名函数,而是使用函数定义或者函数变量的话,在需要多次执行的Promi链中,这些函数也都只有一份在内存中,不被回收也是可以接受的。
我们可以将返回一个什么也不做的Promi封装成一个有语义的函数,以增加代码的可读性:
Promi.cancel = Promi.stop = function() { return new Promi(function(){})}
然后我们就可以这么使用了:
new Promi(function(resolve, reject) { resolve(42)}) .then(function(value) { // "Big ERROR!!!" return Promi.stop() }) .catch() .then() .then() .catch() .then()
看起来是不是有语义的多?
Promi链上返回的最后一个Promi出错了怎么办?
考虑如下代码:
new Promi(function(resolve) { resolve(42)}) .then(function(value) { alter(value) })
乍一看好像没什么问题,但运行这段代码的话你会发现什么现象也不会发生,既不会alert出42,也不会在控制台报错,怎么回事呢。细看最后一行,alert被打成了alter,那为什么控制台也没有报错呢,因为alter所在的函数是被包在try/catch块里的,alter这个变量找不到就直接抛错了,这个错就正好成了then返回的Promi的rejection reason。
也就是说,在Promi链的最后一个then里出现的错误,非常难以发现,有文章指出,可以在所有的Promi链的最后都加上一个catch,这样出错后就能被捕获到,这种方法确实是可行的,但是首先在每个地方都加上几乎相同的代码,违背了DRY原则,其次也相当的繁琐。另外,最后一个catch依然返回一个Promi,除非你能保证这个c20年后的世界atch里的函数不再出错,否则问题依然存在。在Q中有一个方法叫done,把这个方法链到Promi链的最后,它就能够捕获前面未处理的错误,这其实跟在每个链后面加上catch没有太大的区别,只是由框架来做了这件事,相当于它提供了一个不会出错的catch链,我们可以这么实现done方法:
Promi.prototype.done = function(){ return this.catch(function(e) { // 此处一定要确保这个函数不能再出错 console.error(e) })}
可是,能不能在不加catch或者done的情况下,也能够让开发者发现Promi链最后的错误呢?答案依然是肯定的。
我们可以在一个Promi被reject的时候检查这个Promi的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。以下为具体实现:
function reject(reason) { tTimeout(function() { if (lf.status === 'pending') { lf.status = 'rejected' lf.data = reason if (lf.onRejectedCallback.length === 0) { console.error(reason) } for (var i = 0; i < lf.rejectedFn.length; i++) { lf.rejectedFn[i](reason) } } })}
上面的代码对于以下的Promi链也能处理的很好:
new Promi(function(){ // promi1 reject(3)}) .then() // returns promi2 .then() // returns promi3 .then() // returns promi4
看起来,promi1,2,3,4都没有处理函数,那是不是会在控制台把这个错误输出4次呢,并不会,实际上,promi1,2,3都隐式的有处理函数,就是then的默认参数,各位应该还记得then的默认参数最终是被push到了Promi的callback数组里。只有promi4是真的没有任何callback,因为压根就没有调用它的then方法。
事实上,Bluebird和ES6 Promi都做了类似的处理,在Promi被reject但又没有callback时,把错误输出到控制台。
Q使用了done方法来达成类似的目的,$q在最新的版本中也加入了类似的功能。
Angular里的$q跟其它Promi的交互
一般来说,我们不会在Angular里使用其它的Promi,因为Angular已经集成了$q,但有些时候我们在Angular里需要用到其它的库(比如LeanCloud的JS SDK),而这些库或是封装了ES6的Promi,或者是自己实现了Promi,这时如果你在Angular里使用这些库,就有可能发现视图跟Model不同步。究其原因,是因为$q已经集成了Angular的digest loop机制,在Promi被resolve或reject时触发digest,而其它的Promi显然是不会集成的,所以如果你运行下面这样的代码,视图是不会同步的:
app.controller(function($scope) { Promi.resolve(42).then(function(value) { $scope.value = value })})
Promi结束时并不会触发digest,所以视图没有同步。$q上正好有个when方法,它可以把其它的Promi转换成$q的Promi(有些Promi实现中提供了Promi.cast函数,用于将一个thenable转换为它的Promi),问题就解决了:
app.controller(function($scope, $q) { $q.when(Promi.resolve(42)).then(function(value) { $scope.value = value })})
当然也有其它的解决方案比如在其它Promi的链的最后加一个digest,类似下面这样:
Promi.prototype.$digest = function() { $rootScope.$digest() return this}// 然后这么使用OtherPromi .resolve(42) .then(function(value) { $scope.value = value }) .$digest()
因为使用场景并不多,此处不做深入讨论。
出错时,是用throw new Error()还是用return Promi.reject(new Error())呢?
这里我觉得主要从性能和编码的舒适度角度考虑:
性能方面,throw new Error()会使代码进入catch块里的逻辑(还记得我们把所有的回调都包在try/catch里了吧),传说throw用多了会影响性能,因为一但throw,代码就有可能跳到不可预知的位置。
但考虑到onResolved/onRejected函数是直接被包在Promi实现里的try里,出错后就直接进入了这个try对应 的catch块,代码的跳跃“幅度”相对较小,我认为这里的性能损失可以忽略不记。有机会可以测试一下。
而使用Promi.reject(new Error()),则需要构造一个新的Promi对象(里面包含2个数组,4个函数:resolve/reject,onResolved/onRejected),也会花费一定的时间和内存。
而从编码舒适度的角度考虑,出错用throw,正常时用return,可以比较明显的区分出错与正常,throw和return又同为关键字,用来处理对应的情况也显得比较对称(-_-)。另外在一般的编辑器里,Promi.reject不会被高亮成与throw和return一样的颜色。最后,如果开发者又不喜欢构造出一个Error对象的话,Error的高亮也没有了。
综上,我觉得在Promi里发现显式的错误后,用throw抛出错误会比较好,而不是显式的构造一个被reject的Promi对象。
最佳实践
这里不免再啰嗦两句最佳实践
1.一是不要把Promi写成嵌套结构,至于怎么改进,这里就不多说了
// 错误的写法promi1.then(function(value) { promi1.then(function(value) { promi1.then(function(value) { }) })})
2.二是链式Promi要返回一个Promi,而不只是构造一个Promi
// 错误的写法Promi.resolve(1).then(function(){ Promi.resolve(2)}).then(function(){ Promi.resolve(3)})
Promi相关的convenience method的实现
请到这里查看Promi.race, Promi.all, Promi.resolve, Promi.reject等方法的具体实现,这里就不具体解释了,总的来说,只要then的实现是没有问题的,其它所有的方法都可以非常方便的依赖then来实现。
结语
最后,如果你觉得这篇文章对你有所帮助,欢迎分享给你的朋友或者团队,记得注明出处哦~
原文链接:https://github.com/xieranmaya/blog/issues/3
本文发布于:2023-04-03 01:07:52,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/39c3e9128d78c8b7ad4a87ee89eb91ef.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类.doc
本文 PDF 下载地址:剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类.pdf
留言与评论(共有 0 条评论) |