-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类 #3
Comments
来不及全部看完,很好奇! 回头接着看。 |
Great. |
应该是「违背了DRY原则」? |
@xcatliu typo,已改~ |
受教了,谢谢分享 |
THX |
Promise.prototype.then = function(onResolved, onRejected) {
...
if (self.state === 'resolved') {
return new Promise(function(resolve, reject) {
try {
var x = onResolved(self.data);
// 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
if (x instanceof Promise) x.then(resolve, reject);
resolve(x);
... 这里 |
@nobodiness 问题提的很好,不过注意看源代码, try块是包在setTimeout里面的 |
我根据大大的教程实现了一遍。但是在测试框架的时候一直提示我resolvePromise这个参数函数的y参数处理有问题= =。 2.3.3.3.1: If/when
|
ww我找到原因了。 if (typeof then === 'function') {
let hasBeenCalled = false; // BOOOOM!!!!! 这里局部变量导致调用一次的flag没有暴露在判断Promise的代码块中。
// 把他提到函数顶部即可解决问题。
try {
then.call(returnValue, (y)=> {
if (hasBeenCalled)return;
hasBeenCalled = true;
resolveProomise(proomise2, y, resolve, reject);
}, (r)=> {
if (hasBeenCalled)return;
hasBeenCalled = true;
reject(r);
});
} catch (error) {
if (hasBeenCalled) {
// ignore the error
} else {
reject(error);
}
}
} else {
resolve(returnValue);
} |
@HeskeyBaozi 这个问题在我最初实现的时候也是纠结了我好久,最终跟你差不多,发现还是自己没有考虑全面~~我的代码里面也有几乎相同的逻辑,你在当前页面搜“thenCalledOrThrow”就能看到了~ |
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 |
是的,这个我文中有提到。 |
面试的时候面试官让我手写一个Promise的实现,当时实在不会,现在回来赶紧学习,学到了 |
@xieranmaya 照着楼主的思路自己实现了一下,有一处代码比较有疑惑, function resolvePromise(promise2, x, resolve, reject) {
...
if (x.status === 'pending') { //because x could resolved by a Promise Object
x.then(function(v) {
resolvePromise(promise2, v, resolve, reject)
}, reject)
...
} 注释中写到 because x could resolved by a Promise Object 可是在 Promise 的构造里面不是写了 if (value instanceof MyPromise) {
return value.then(resolve, reject);
} 这样最后 resolve 调用时候不是一定不是一个 望指点~ |
@zhuscat 第一段代码的意思是 第二段代码应该是处理类似 x.then(p2 => {
return new Promise(...)
}) 这样的情况的。 |
@think2011 有点没理解。 第一段代码考虑的不是 if (value instanceof MyPromise) {
return value.then(resolve, reject);
} |
mark |
写的不错!很详细 |
非常详细,我自己实现了一小半都写不下去了,佩服作者 |
同问,我也没理解这一段@zhuscat |
是不是应该在三种状态下执行try成功后,都应该执行resolve函数呢 以确保后面都链式操作继续执行 |
似乎不能使用class实现Promise,我照着大佬的代码写了一遍class版本的,并不能通过测试脚本,我找了很久的问题,也没有找到。 class myPromise {
PENDING = 'pending'
RESOLVE = 'resolve'
REJECT = 'reject'
_data = undefined
_resolvedCallBacks = []
_rejectedCallBacks = []
_status = this.PENDING
constructor (handler) {
try {
handler(this._resolve.bind(this), this._reject.bind(this))
} catch (e) {
this._reject(e)
}
}
_resolve (value) {
setTimeout(() => {
if (this._status == this.PENDING) {
this._status = this.RESOLVE
this._data = value
for (var i = 0; i < this._resolvedCallBacks.length; i++) {
this._resolvedCallBacks[i](value)
}
}
})
}
_reject (value) {
setTimeout(() => {
if (this.status == this.PENDING) {
this._status = this.REJECT
this._data = value
for (var i = 0; i < this._rejectedCallBacks.length; i++) {
this._rejectedCallBacks[i](value)
}
}
})
}
resolvePromise (promise2, x, resolve, reject) {
let then
let thenCalledOrThrow = false
let self = this
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise!'))
}
if (x instanceof myPromise) {
if (x._status === 'pending') {
x.then(function(value) {
self.resolvePromise(promise2, value, resolve, reject)
}, reject)
} else {
x.then(resolve, reject)
}
return
}
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
then = x.then
if (typeof then === 'function') {
then.call(x, function rs(y) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return self.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)
}
}
then (onResolve, onReject) {
let self = this
let promise2
onResolve = typeof onResolve === 'function' ? onResolve : function(value) { return value }
onReject = typeof onReject === 'function' ? onReject : function(reason) { return reason }
if (self._status == this.RESOLVE) {
return promise2 = new myPromise(function(resolve, reject) {
setTimeout(function() {
try {
let x = onResolve(self._data)
self.resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
if (self._status == this.REJECT) {
return promise2 = new myPromise(function(resolve, reject) {
setTimeout(function() {
try {
let x = onReject(self._data)
self.resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
if (self._status == this.PENDING) {
return promise2 = new myPromise(function(resolve, reject) {
self._resolvedCallBacks.push(function(value) {
try {
let x = onResolve(value)
self.resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
self._rejectedCallBacks.push(function(value) {
try {
let x = onReject(value)
self.resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
})
})
}
}
} |
我的理解是,红框部分应该是有三个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时要传递的值了 |
大佬,有个问题请教下。 self.onResolvedCallback = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
self.onRejectedCallback = [] // Promise reject时的回调函数集,因为在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 value;
x.then(resolve, reject)
}
return
}
|
var p = new Promise(resolve => {
setTimeout(resolve, 1000, 'hi')
});
p.then(() => {
console.log(1)
})
p.then(() => {
console.log(2)
}) |
mark~~!! |
测试发现 resolve 函数里面的判断是没必要的,忽略掉就好 |
当 promise 对象已经处非 pending 的状态时,就没法调用 resolve 或 reject 方法了呀,所以也就不存在一起执行的条件。这时 promise 的 executor 已经处理完成任务并且把运行结果挂在 value 上了,那么就直接从 value 里取出来然后直接运行回调 |
process.nextTick(()=>{}) |
你好,我遇到了同样的问题,但是我不是很能理解你说的放到函数顶部。我理解递归调用的时候,这个判断调用一次的flag在每次调用的时候都会被重置为false |
你好,我想请问一下resolve和reject函数内部的实现为什么是需要整个函数体放到setTimeout。我理解状态的改变从pending到fulfilled或者rejected是一个同步的操作。但是实际上我尝试把状态这部分代码放到setTimeout外部之后测试用例就无法通过了。 const a = new Promise((resolve, reject) => {
resolve(1);
});
a.then(() => {
console.log(a, 111);
});
console.log(a); 实际上浏览器的Promise打出来的结果是
而实现版打出来的结果是 Promise {<pennding>}
Promise {<fulfilled>: 1} 111 我理解是不是因为浏览器的状态改变是同步操作的结果。希望大佬能帮我解答下问题 |
这个纯粹是语法错误了。class 和构造函数还是有区别的,怎么可以直接在方法外面编写执行语句?运行测试的时候应该会报错 unexcepted token "=" |
mark |
大佬,为什么then里面还有pedding状态的判断呢 |
上述实现的promise状态和结果是可以被改变的,有什么方法可以解决吗? |
使用getter/setter设置状态值即可 |
tql~ |
这是来自QQ邮箱的假期自动回复邮件。
您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。
|
本文适用的读者
本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,建议先了解Promise的使用
Promise标准解读
只有一个
then
方法,没有catch
,race
,all
等方法,甚至没有构造函数Promise标准中仅指定了Promise对象的
then
方法的行为,其它一切我们常见的方法/函数都并没有指定,包括catch
,race
,all
等常用方法,甚至也没有指定该如何构造出一个Promise对象,另外then也没有一般实现中(Q, $q等)所支持的第三个参数,一般称onProgressthen
方法返回一个新的PromisePromise的
then
方法返回一个新的Promise,而不是返回this,此处在下文会有更多解释不同Promise的实现需要可以相互调用(interoperable)
Promise的初始状态为pending,它可以由此状态转换为fulfilled(本文为了一致把此状态叫做resolved)或者rejected,一旦状态确定,就不可以再次转换为其它状态,状态确定的过程称为settle
更具体的标准见这里
一步一步实现一个Promise
下面我们就来一步一步实现一个Promise
构造函数
因为标准并没有指定如何构造一个Promise对象,所以我们同样以目前一般Promise实现中通用的方法来构造一个Promise对象,也是ES6原生Promise里所使用的方式,即:
我们先实现构造函数的框架如下:
上面的代码基本实现了Promise构造函数的主体,但目前还有两个问题:
我们给executor函数传了两个参数:resolve和reject,这两个参数目前还没有定义
executor有可能会出错(throw),类似下面这样,而如果executor出错,Promise应该被其throw出的值reject:
所以我们需要在构造函数里定义resolve和reject这两个函数:
有人可能会问,resolve和reject这两个函数能不能不定义在构造函数里呢?考虑到我们在executor函数里是以
resolve(value)
,reject(reason)
的形式调用的这两个函数,而不是以resolve.call(promise, value)
,reject.call(promise, reason)
这种形式调用的,所以这两个函数在调用时的内部也必然有一个隐含的this,也就是说,要么这两个函数是经过bind后传给了executor,要么它们定义在构造函数的内部,使用self来访问所属的Promise对象。所以如果我们想把这两个函数定义在构造函数的外部,确实是可以这么写的:但是众所周知,bind也会返回一个新的函数,这么一来还是相当于每个Promise对象都有一对属于自己的
resolve
和reject
函数,就跟写在构造函数内部没什么区别了,所以我们就直接把这两个函数定义在构造函数里面了。不过话说回来,如果浏览器对bind的所优化,使用后一种形式应该可以提升一下内存使用效率。另外我们这里的实现并没有考虑隐藏this上的变量,这使得这个Promise的状态可以在executor函数外部被改变,在一个靠谱的实现里,构造出的Promise对象的状态和最终结果应当是无法从外部更改的。
接下来,我们实现
resolve
和reject
这两个函数基本上就是在判断状态为pending之后把状态改为相应的值,并把对应的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的状态和值为己用,但考虑如下代码:此处如果
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方法:
Promise总共有三种可能的状态,我们分三个if块来处理,在里面分别都返回一个new Promise。
根据标准,我们知道,对于如下代码,promise2的值取决于then里面函数的返回值:
如果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标准中所涉及到的内容,但还有几个问题:
不同的Promise实现之间需要无缝的可交互,即Q的Promise,ES6的Promise,和我们实现的Promise之间以及其它的Promise实现,应该并且是有必要无缝相互调用的,比如:
我们前面实现的代码并没有处理这样的逻辑,我们只判断了onResolved/onRejected的返回值是否为我们实现的Promise的实例,并没有做任何其它的判断,所以上面这样的代码目前是没有办法在我们的Promise里正确运行的。
下面这样的代码目前也是没办法处理的:
正确的行为应该是alert出8,而如果拿我们的Promise,运行上述代码,将会alert出undefined。这种行为称为穿透,即8这个值会穿透两个then(说Promise更为准确)到达最后一个then里的foo函数里,成为它的实参,最终将会alert出8。
下面我们首先处理简单的情况,值的穿透
Promise值的穿透
通过观察,会发现我们希望下面这段代码
跟下面这段代码的行为是一样的
所以如果想要把then的实参留空且让值可以穿透到后面,意味着then的两个参数的默认值分别为
function(value) {return value}
,function(reason) {throw reason}
。所以我们只需要把then里判断
onResolved
和onRejected
的部分改成如下即可:于是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实现中找到类似的代码:
然后我们使用这个函数的调用替换then里几处判断x是否为Promise对象的位置即可,见下方完整代码。
最后,我们刚刚说到,原则上,
promise.then(onResolved, onRejected)
里的这两相函数需要异步调用,关于这一点,标准里也有说明:所以我们需要对我们的代码做一点变动,即在四个地方加上setTimeout(fn, 0),这点会在完整的代码中注释,请各位自行发现。
事实上,即使你不参照标准,最终你在自测试时也会发现如果then的参数不以异步的方式调用,有些情况下Promise会不按预期的方式行为,通过不断的自测,最终你必然会让then的参数异步执行,让executor函数立即执行。本人在一开始实现Promise时就没有参照标准,而是自己凭经验测试,最终发现的这个问题。
至此,我们就实现了一个的Promise,完整代码如下:
测试
如何确定我们实现的Promise符合标准呢?Promise有一个配套的测试脚本,只需要我们在一个CommonJS的模块中暴露一个deferred方法(即exports.deferred方法),就可以了,代码见上述代码的最后。然后执行如下代码即可执行测试:
关于Promise的其它问题
Promise的性能问题
可能各位看官会觉得奇怪,Promise能有什么性能问题呢?并没有大量的计算啊,几乎都是处理逻辑的代码。
理论上说,不能叫做“性能问题”,而只是有可能出现的延迟问题。什么意思呢,记得刚刚我们说需要把4块代码包在setTimeout里吧,先考虑如下代码:
运行上面的代码,会打印出多少次'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里的函数):
假设这个
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
状态,后面的代码也就一直不会执行了,具体代码如下:这种方式看起来有些山寨,它也确实解决了问题。但它引入的一个新问题就是链式调用后面的所有回调函数都无法被垃圾回收器回收(在一个靠谱的实现里,Promise应该在执行完所有回调后删除对所有回调函数的引用以让它们能被回收,在前文的实现里,为了减少复杂度,并没有做这种处理),但如果我们不使用匿名函数,而是使用函数定义或者函数变量的话,在需要多次执行的Promise链中,这些函数也都只有一份在内存中,不被回收也是可以接受的。
我们可以将返回一个什么也不做的Promise封装成一个有语义的函数,以增加代码的可读性:
然后我们就可以这么使用了:
看起来是不是有语义的多?
Promise链上返回的最后一个Promise出错了怎么办?
考虑如下代码:
乍一看好像没什么问题,但运行这段代码的话你会发现什么现象也不会发生,既不会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方法:
可是,能不能在不加catch或者done的情况下,也能够让开发者发现Promise链最后的错误呢?答案依然是肯定的。
我们可以在一个Promise被reject的时候检查这个Promise的onRejectedCallback数组,如果它为空,则说明它的错误将没有函数处理,这个时候,我们需要把错误输出到控制台,让开发者可以发现。以下为具体实现:
上面的代码对于以下的Promise链也能处理的很好:
看起来,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显然是不会集成的,所以如果你运行下面这样的代码,视图是不会同步的:
Promise结束时并不会触发digest,所以视图没有同步。$q上正好有个when方法,它可以把其它的Promise转换成$q的Promise(有些Promise实现中提供了Promise.cast函数,用于将一个thenable转换为它的Promise),问题就解决了:
当然也有其它的解决方案比如在其它Promise的链的最后加一个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对象。最佳实践
这里不免再啰嗦两句最佳实践
一是不要把Promise写成嵌套结构,至于怎么改进,这里就不多说了
二是链式Promise要返回一个Promise,而不只是构造一个Promise
Promise相关的convenience method的实现
请到这里查看
Promise.race
,Promise.all
,Promise.resolve
,Promise.reject
等方法的具体实现,这里就不具体解释了,总的来说,只要then的实现是没有问题的,其它所有的方法都可以非常方便的依赖then来实现。结语
最后,如果你觉得这篇文章对你有所帮助,欢迎分享给你的朋友或者团队,记得注明出处哦~
The text was updated successfully, but these errors were encountered: