深入理解 Promise(下)
如何终止 Promise
在 promise 的链式调用中,涉及到一个类似 break 的操作。就是在某一个 then 函数的调用中,某种情况下,要取消后续的所有操作。即跳出本次 promise,不继续执行后面的 then 方法。但 本次的 promise 依旧会继续执行。
然而Promise本身并没有提供这样的功能,一个操作,要么成功,要么失败,要么跳转到then里,要么跳转到catch里。
方案一、
抛出一个特殊的Error对象,然后在Promise链后面的所有catch回调里,检查传来的错误是否为该类型的错误,如果是,就一直往后抛。直到抛到 Promise 链的尾部。
function start() {
return new Promise((resolve, reject) => {
resolve('start');
});
}
start()
.then(data => {
console.log('resolve,result of start: ', data);
return Promise.reject(1); // p1
})
.then(data => {
console.log('resolve,result of p1: ', data);
return Promise.reject(2); // p2
})
.then(data => {
console.log('result of p2: ', data);
return Promise.resolve(3); // p3
}, data => {
console.log('reject, result of p1: ', data);
return Promise.reject(3); // p3
})
.catch(ex => {
console.log('ex: ', ex);
return Promise.resolve(4); // p4
})
.then(data => {
console.log('result of p4: ', data);
});
看下这个例子,其中由于 p2 的那个 then 方法中没有 reject 处理函数,所以被跳过了。
要知道 catch 是对 then(null, rejectHandler) 的一种封装(语法糖)。所以,有 reject 处理函数的 then 里面的 reject 处理函数可以不写换成 catch。
所以,如果我们只向 then 方法中传入 resolve 处理函数,在 promise 调用链的最后加上 catch,把所有的错误处理都放在 catch 里面,想要 break 的时候抛出一个固定的错误,就能达到终止操作的效果了。
catch 主要是用来做异常处理的,而终止 promise 需要人为抛出错误。为了让这两者分开,需要对终止 Promise 的错误做一个标识。好让开发人员知道这是一个程序异常,还是终止Promise异常。
start()
.then(data => {
// promise start
console.log('result of start: ', data);
return Promise.resolve(1); // p1
})
.then(data => {
// promise p1
console.log('result of p1: ', data);
return Promise.reject({
notRealPromiseException: true,
}); // p2
})
.then(data => {
// promise p2
console.log('result of p2: ', data);
return Promise.resolve(3); // p3
})
.catch(ex => {
console.log('ex: ', ex);
if (ex.notRealPromiseException) {
// 一切正常,只是通过 catch 方法来中止 promise chain
// 也就是中止 promise p2 的执行
return true;
}
// 真正发生异常
return false;
});
现在我们是在 Promise 链尾部用 catch 来捕获 终止Promise 的错误,即一个 Promise链中间不允许写失败回调处理函数(then的第二个参数 or catch)。那么,如果我 Promise 链中间写了失败处理函数(then的第二个参数 or catch)呢。
start()
.then(data => {
console.log('resolve,result of start: ', data);
return Promise.reject(1); // p1
})
.then(data => {
console.log('resolve,result of p1: ', data);
return Promise.reject(2); // p2
})
.then(data => {
console.log('result of p2: ', data);
return Promise.resolve(3); // p3
}, data => {
console.log('reject, result of p1: ', data);
})
.catch(ex => {
console.log('ex: ', ex);
})
.then(data => {
console.log('result of p4: ', data); // 或被执行 打印的是 undefined 因为这里 catch 没有返回值 即 return undefined
});
因为 catch 执行完返回的也是一个新的 Promise 对象,所以,catch 后续的 then 依旧会继续执行,所以这种情况下,我们需要 catch 如果捕获到了 终止 Promise 的错误,需要把这个错误接着往下抛,使它直接去找下一个失败处理函数(catch),而不再执行后续的 then 了。类似这样:
doSth()
.then(value => {
if (sthErrorOccured()) {
throw new Error('BIG_ERROR')
}
// normal logic
})
.catch(reason => {
if (reason.message === 'BIG_ERROR') {
throw reason
}
// normal logic
})
.then()
.catch(reason => {
if (reason.message === 'BIG_ERROR') {
throw reason
}
// normal logic
})
.then()
.catch(reason => {
if (reason.message === 'BIG_ERROR') {
throw reason
}
// normal logic
})
即在Promise链后面的所有catch回调里,检查传来的错误是否为该终止 Promise 类型的错误,如果是,就一直往后抛。以达到不会执行后续所有的 then 。
这种方案的问题在于,你需要在每一个catch里多写一个if来判断这个特殊的Error,繁琐不说,还增加了耦合度以及重构的困难。
方案二、
有没有不用 catch 去检测的方式,来终止 Promise 链呢。那就是在需要终止的地方,直接返回一个始终不resolve也不reject的Promise,即这个Promise永远处于pending状态,那么后面的Promise链当然也就一直不会执行了,因为会一直等着。
Promise.stop = function() {
return new Promise(function(){})
}
doSth()
.then(value => {
if (sthBigErrorOccured()) {
return Promise.stop() // new Promise(function(){})
}
// normal logic
})
.catch(reason => {// will never get called
// normal logic
})
.then()
.catch(reason => {// will never get called
// normal logic
})
.then()
.catch(reason => {// will never get called
// normal logic
})
这种方案的好处在于你几乎不需要更改任何现有代码,而且兼容性也非常好,不管你使用的哪个Promise库,甚至是不同的Promise之间相互调用,都可以达到目的。
然而这个方案有一个不那么明显的缺陷,那就是会造成潜在的内存泄露。
试想,当你把回调函数传给Promise的then方法后,如果这时Promise的状态还没有确定下来,那么Promise实例肯定会在内部保留这些回调函数的引用;在一个robust的实现中,回调函数在执行完成后,Promise实例应该会释放掉这些回调函数的引用。如果使用上述方案,那么返回一个永远处于pending状态的Promise之后的Promise链上的所有Promise都将处于pending状态,这意味着后面所有的回调函数的内存将一直得不到释放。在简单的页面里使用这种方案也许还行得通,但在WebApp或者Node里,这种方案明显是不可接受的。
那有没有办法即达到停止后面的链,同时又避免内存泄露呢。
结合方案一、在方案二的基础上对 then 处理函数加一个判断,判断上一次异步操作的结果是不是 终止Promise(返回一个处于pending状态的Promis) 的操作,如果是,则直接返回一个 普通值,不再返回一个 Promise 对象。这样就能终止后续的链式调用了。
这种方式需要重写 then 。即 then 并总是返回一个 新的 Promise 对象,对于上次异步操作的结果返回的是一个终止 Promise 的标识,then 就返回一个普通值。
(function() {
let STOP_VALUE = {}; // //只要外界无法“===”这个对象就可以了
// let STOP_VALUE = Symbol()//构造一个Symbol以表达特殊的语义
let STOP_PROMISE = Promise.resolve(STOP_VALUE); // //不是每次返回一个新的Promise,可以节省内存。即所有的终止 Promise 而返回的永远处于pedding状态的Promise对象都是共用这一个 Promise,不需要每次终止 Promise 都去返回一个新的永远处于pedding状态的Promise对象
Promise.stop = function () {
return STOP_PROMISE;
}
Promise.prototype._then = Promise.prototype.then;
Promise.prototype.then = function(resolveFun, rejectedFun) {
return this._then((value) => {
return value === STOP_VALUE ? value : resolveFun(value);
}, rejectedFun); // return
};
})()
Promise.resolve(8).then(v => {
console.log(v)
return 9
}).then(v => {
console.log(v)
return Promise.stop()//较为明确的语义
}).catch(function(){// will never called but will be GCed
console.log('catch')
}).then(function(){// will never called but will be GCed
console.log('then')
})
重写 then, 闭包里的对象在外界是访问不到,外界也永远也无法构造出一个跟闭包里Symbol一样的对象,所以可以用它来标识 终止Promise 。因为任何正常 Promise 的结果永远不会与 Symbol 这个对象相等,不会把正常的Promise结果当做终止Promise操作来执行。所以可以用它来标识 Promise 的终止操作。考虑到我们只需要构造一个外界无法“===”的对象,我们完全可以用一个Object来代替。
Reference
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 排序算法之堆排序
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论