在使用Promise处理一些复杂逻辑的过程中,我们有时候会想要在发生某种错误后就停止执行Promise链后面所有的代码。

然而Promise本身并没有提供这样的功能,一个操作,要么成功,要么失败,要么跳转到then里,要么跳转到catch里。

如果非要处理这种逻辑,一般的想法是抛出一个特殊的Error对象,然后在Promise链后面的所有catch回调里,检查传来的错误是否为该类型的错误,如果是,就一直往后抛,类似下面这样

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
})

这种方案的问题在于,你需要在每一个catch里多写一个if来判断这个特殊的Error,繁琐不说,还增加了耦合度以及重构的困难。

如果有什么办法能直接在发生这种错误后停止后面所有Promise链的执行,我们就不需要在每个catch里检测这种错误了,只需要编写处理该catch块本应处理的错误的代码就可以了。

有没有办法不在每个catch里做这种判断呢?

办法确实是有的,那就是在发生无法继续的错误后,直接返回一个始终不resolve也不reject的Promise,即这个Promise永远处于pending状态,那么后面的Promise链当然也就一直不会执行了,因为会一直等着。类似下面这样的代码

Promise.stop = function() {
return new Promise(function(){})
} doSth()
.then(value => {
if (sthBigErrorOccured()) {
return Promise.stop()
}
// 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里,这种方案明显是不可接受的。

Promise.stop = function() {
return new Promise(function(){})
} doSth()
.then(value => {
if (sthBigErrorOccured()) {
return Promise.stop()
}
// normal logic
})
.catch(reason => {// this function will never got GCed
// normal logic
})
.then()
.catch(reason => {// this function will never got GCed
// normal logic
})
.then()
.catch(reason => {// this function will never got GCed
// normal logic
})

那有没有办法即达到停止后面的链,同时又避免内存泄露呢。

让我们回到一开始的思路,我们在Promise链上所有的catch里都加上一句if,来判断传来的错误是否为一个无法处理的错误,如果是则一直往后面抛,这样就达到了即没有运行后面的逻辑,又避免了内存泄露的问题。

这是一个高度一致的逻辑,我们当然可以把它抽离出来。我们可以实现一个叫next的函数,挂在Promise.prototype上面,然后在里面判断是否是我们能处理的错误,如果是,则执行回调,如果不是,则一直往下传:

var BIG_ERROR = new Error('BIG_ERROR')

Promise.prototype.next = function(onResolved, onRejected) {
return this.then(function(value) {
if (value === BIG_ERROR) {
return BIG_ERROR
} else {
return onResolved(value)
}
}, onRejected)
} doSth()
.next(function(value) {
if (sthBigErrorOccured()) {
return BIG_ERROR
}
// normal logic
})
.next(value => {
// will never get called
})

进一步,如果把上面代码中“致命错误”的语义换成“跳过后面所有的Promise”,我们就可以得到跳过后续Promise的方式了:

var STOP_SUBSEQUENT_PROMISE_CHAIN = new Error()

Promise.prototype.next = function(onResolved, onRejected) {
return this.then(function(value) {
if (value === STOP_SUBSEQUENT_PROMISE_CHAIN) {
return STOP_SUBSEQUENT_PROMISE_CHAIN
} else {
return onResolved(value)
}
}, onRejected)
} doSth()
.next(function(value) {
if (sthBigErrorOccured()) {
return STOP_SUBSEQUENT_PROMISE_CHAIN
}
// normal logic
})
.next(value => {
// will never get called
})

为了更明显的语义,我们可以把“跳过后面所有的Promise”单独封装成一个Promise:

var STOP = {}
Promise.stop = function(){
return Promise.resolve(STOP)
} Promise.prototype.next = function(onResolved, onRejected) {
return this.then(function(value) {
if (value === STOP) {
return STOP
} else {
return onResolved(value)
}
}, onRejected)
} doSth()
.next(function(value) {
if (sthBigErrorOccured()) {
return Promise.stop()
}
// normal logic
})
.next(value => {
// will never get called
})

这样就实现了在语义明确的情况下,不造成内存泄露,而且还停止了后面的Promise链。

为了对现有代码尽量少做改动,我们甚至可以不用新增next方法而是直接重写then:

(function() {
var STOP_VALUE = Symbol()//构造一个Symbol以表达特殊的语义
var STOPPER_PROMISE = Promise.resolve(STOP_VALUE) Promise.prototype._then = Promise.prototype.then Promise.stop = function() {
return STOPPER_PROMISE//不是每次返回一个新的Promise,可以节省内存
} Promise.prototype.then = function(onResolved, onRejected) {
return this._then(function(value) {
return value === STOP_VALUE ? STOP_VALUE : onResolved(value)
}, onRejected)
}
}()) 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一样的对象,考虑到我们只需要构造一个外界无法“===”的对象,我们完全可以用一个Object来代替:

(function() {
var STOP_VALUE = {}//只要外界无法“===”这个对象就可以了
var STOPPER_PROMISE = Promise.resolve(STOP_VALUE) Promise.prototype._then = Promise.prototype.then Promise.stop = function() {
return STOPPER_PROMISE//不是每次返回一个新的Promise,可以节省内存
} Promise.prototype.then = function(onResolved, onRejected) {
return this._then(function(value) {
return value === STOP_VALUE ? STOP_VALUE : onResolved(value)
}, onRejected)
}
}()) 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')
})

这个方案的另一个好处(好处之一是不会造成内存泄露)是可以让你非常平滑地(甚至是一次性的)从“返回一个永远pending的Promise”过度到这个方案,因为代码及其语义都基本没有变化。在之前,你可以定义一个Promise.stop()方法来返回一个永远pending的Promise;在之后,Promise.stop()返回一个外界无法得到的值,用以表达“跳过后面所有的Promise”,然后在我们重写的then方法里使用。

这样就解决了停止Promise链这样一个让人纠结的问题。

在考察了不同的Promise实现后,我发现Bluebird和浏览器原生Promise都可以在Promise.prototype上直接增加实例方法,但Q和$q(Angular)却不能这么做,具体要在哪个子对象的原型上加或者改方法我就没有深入研究了,但相信肯定是有办法的。

可是这篇文章如果到这里就结束的话,就显得太没有意思了~~

顺着上面的思路,我们甚至可以实现Promise链的多分支跳转。

我们知道,Promise链一般来说只支持双分支跳转。

按照Promise链的最佳写法实践,处理成功的回调只用then的第一个参数注册,错误处理的回调只使用catch来注册。这样在任意一个回调里,我们可以通过return或者throw(或者所返回Promise的最终状态的成功与否)跳转到最近的then或者catch回调里:

doSth()
.then(fn1)
.catch(fn2)
.catch(fn3)
.then(fn4)
.then(fn5)
.catch(fn6)

以上代码中,任意一个fn都只能选择往后跳到最近一then或者catch的回调里。

但在实际的使用的过程中,我发现双分支跳转有时满足不了我的需求。如果能在不破坏Promise标准的前提下让Promise实现多分支跳转,将会对复杂业务代码的可读性以及可维护性有相当程度的提升。

顺着上面的思路,我们可以在Promise上定义多个有语义的函数,在Promise.prototype上定义对应语义的实例方法,然后在实例方法中判断传来的值,然后根据条件来执行或者不执行该回调,当这么说肯定不太容易明白,我们来看代码分析:

(function() {
var STOP = {}
var STOP_PROMISE = Promise.resolve(STOP)
var DONE = {}
var WARN = {}
var ERROR = {}
var EXCEPTION = {}
var PROMISE_PATCH = {} Promise.prototype._then = Promise.prototype.then//保存原本的then方法 Promise.prototype.then = function(onResolved, onRejected) {
return this._then(result => {
if (result === STOP) {// 停掉后面的Promise链回调
return result
} else {
return onResolved(result)
}
}, onRejected)
} Promise.stop = function() {
return STOP_PROMISE
} Promise.done = function(value) {
return Promise.resolve({
flag: DONE,
value,
})
} Promise.warn = function(value) {
return Promise.resolve({
flag: WARN,
value,
})
} Promise.error = function(value) {
return Promise.resolve({
flag: ERROR,
value,
})
} Promise.exception = function(value) {
return Promise.resolve({
flag: EXCEPTION,
value,
})
} Promise.prototype.done = function(cb) {
return this.then(result => {
if (result && result.flag === DONE) {
return cb(result.value)
} else {
return result
}
})
} Promise.prototype.warn = function(cb) {
return this.then(result => {
if (result && result.flag === WARN) {
return cb(result.value)
} else {
return result
}
})
} Promise.prototype.error = function(cb) {
return this.then(result => {
if (result && result.flag === ERROR) {
return cb(result.value)
} else {
return result
}
})
} Promise.prototype.exception = function(cb) {
return this.then(result => {
if (result && result.flag === EXCEPTION) {
return cb(result.value)
} else {
return result
}
})
}
})()

然后我们可以像下面这样使用:

new Promise((resolve, reject) => {
// resolve(Promise.stop())
// resolve(Promise.done(1))
// resolve(Promise.warn(2))
// resolve(Promise.error(3))
// resolve(Promise.exception(4))
})
.done(value => {
console.log(value)
return Promise.done(5)
})
.warn(value => {
console.log('warn', value)
return Promise.done(6)
})
.exception(value => {
console.log(value)
return Promise.warn(7)
})
.error(value => {
console.log(value)
return Promise.error(8)
})
.exception(value => {
console.log(value)
return
})
.done(value => {
console.log(value)
return Promise.warn(9)
})
.warn(value => {
console.log(value)
})
.error(value => {
console.log(value)
})

以上代码中:

  • 如果运行第一行被注释的代码,这段程序将没有任何输出,因为所有后面的链都被“停”掉了
  • 如果运行第二行被注释的代码,将输出1 5 9
  • 如果运行第三行被注释的代码,将输出2 6 9
  • 如果运行第四行被注释的代码,将输出3 8
  • 如果运行第五行被注释的代码,将输出4 7

即return Promise.done(value)将跳到最近的done回调里

依次类推。

这样就实现了Promise链的多分支跳转。针对不同的业务,可以封装出不同语义的静态方法和实例方法,实现任意多的分支跳转。

但这个方案目前有一点不足,就是不能用then来捕获任意分支:

new Promise((resolve) => {
resolve(Promise.warn(2))
})
.then(value => { })
.warn(value => { })

这种写法中,从语义或者经验上讲,then应该捕获前面的任意值,然而经过前面的改动,这里的then将捕获到这样的对象:

{
flag: WARN,
value: 2
}

而不是2,看看前面的代码就明白了:

Promise.prototype.then = function(onResolved, onRejected) {
return this._then(result => {
if (result === STOP) {
return result
} else {
return onResolved(result)// 将会走这条分支,而此时result还是被包裹的对象
}
}, onRejected)
}

目前我还没有找到比较好的方案,试了几种都不太理想(也许代码写丑一点可以实现,但我并不想这么做)。所以只能在用到多分支跳转时不用then来捕获传来的值。

不过从有语义的回调跳转到then是可以正常工作的:

doSth()
.warn()
.done()
.exception()
.then()
.then()
.catch()

同样还是可以根据上面的代码看出来。

最后,此文使用到的一个anti pattern是对原生对象做了更改,这在一般的开发中是不被推荐的,本文只是提供一个思路。在真正的工程中,可以继承Promise类以达到几乎相同的效果,此处不再熬述。

多谢各位同僚的阅读,如有纰漏之处还请留言指正~

原文链接:https://github.com/xieranmaya/blog/issues/5

从如何停掉 Promise 链说起的更多相关文章

  1. 如何在Promise链中共享变量?

    译者按: 使用Promise写过异步代码的话,会发现在Promise链中共享变量是一个非常头疼的问题,这也是Async/Await胜过Promise的一点,我们在Async/Await替代Promis ...

  2. 正确停掉 expdp 或 impdp

    1.查看视图dba_datapump_jobs SQL> set line 200 SQL> select job_name,state from dba_datapump_jobs; J ...

  3. 停掉一台服务器,Nginx响应慢(转载)

    测试发现的问题及解决办法 1.当后端两台IIS应用服务器都正常时,访问速度非常快,查看日志,原来一个请求,是后端两台服务器同时响应的; 2.为了模仿故障测试,停掉一台IIS应用服务器,这时再访问,请求 ...

  4. 使用Promise链式调用解决多个异步回调的问题

    使用Promise链式调用解决多个异步回调的问题 比如我们平常经常遇到的一种情况: 网站中需要先获取用户名,然后再根据用户名去获取用户信息.这里获取用户名getUserName()和获取用户信息get ...

  5. 从 axios 源码中了解到的 Promise 链与请求的取消

    axios 中一个请求取消的示例: axios 取消请求的示例代码 import React, { useState, useEffect } from "react"; impo ...

  6. SpringBoot生命周期管理之停掉应用服务几种方法

    前言 在生产环境下管理Spring Boot应用的生命周期非常重要.Spring容器通过ApplicationContext处理应用服务的所有的beans的创建.初始化.销毁. 本文着重于生命周期中的 ...

  7. 中断或取消Promise链的可行方案

    ES6标准引入的异步编程解决方案Promise,能够将层层嵌套的回调转化成扁平的Promise链式调用,优雅地解决了“回调地狱”的问题.当Promise链中抛出一个错误时,错误信息沿着链路向后传递,直 ...

  8. mac 的 ping 命令怎么停掉?

    Widnows下的Ping不会只Ping4次,mac 会不停的Ping下去,需要停止,按下键盘上的 control+c 键即可停掉 Ping 过程.

  9. systemctl无法停掉keepalived

    这个问题搞了好半天,记录一下,启停都是用的systemctl 起初是测试vip漂移时候发现,主备节点都开启keepalived的状况下,一切正常,主节点的vip也可以访问. 第一次停掉主节点的keep ...

随机推荐

  1. noip模拟赛 业务办理

    [问题描述]在银行柜台前,有 n 个顾客排队办理业务. 队伍中从前往后,第 i 位顾客办理业务需要ti 分钟时间. 一位顾客的等待时间定义为:队伍中在他之前的所有顾客和他自己的办理业务时间的总和.第 ...

  2. node.js 发布订阅模式

    //导入内置模块 let EventEmitter = require('events'); let util=require('util'); //Man继承EventEmitter util.in ...

  3. [K/3Cloud] KSQL 关联表更新字段Update语法

    关联表更新字段 UPDATE tmp369faa3f7d224b0595670425008 as t1 SET FStatus=-1 where exists(select 1 from t_BD_S ...

  4. java 远程调用

    1.webservice (soap+http) -aixs -axis2 -xfire—>cxf 2.webservice问题 -基于xml,xml效率,(数据传输效率,xml序列化效率) - ...

  5. BAT经典面试题,深入理解Java内存模型JMM

    Java 内存模型 Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段.静态字段和构成数组对象的元素)的访问方式.试图屏 ...

  6. OSX: node中安装zeromq

    1. brew install pkg-config2. brew install zmq3. export PKG_CONFIG_PATH="/usr/local/lib/pkgconfi ...

  7. 最老程序猿创业开发实训1---Android应用架构之MVC

    我们都知道Android中基本组件是Activity,每一个界面都是一个Activity,自从2.3版本号開始.又添加了Fragment组件,提供了适应于各种屏幕方法.可是因为Android系统仅仅是 ...

  8. 练习使用Trim()函数规范名字输入

    Java中的Trim()函数能够去除字符串的空白前缀和空白后缀,可用来规范用户输入的内容,详细这样用: String s="   Hello world  ".trim(); 然后 ...

  9. socket实现web server

    http://blog.csdn.net/u012734441/article/details/44801523 很好的一个例子,现在java程序原开发web服务端程序基本上都使用现成的web框架,这 ...

  10. research plan2222

    Thank you for calling. I've been looking forward to this call for a long time.Now, let me introduce ...