我们经常会遇到这种情况:比如通过用户名查找并返回该用户信息和他的关注者。通常有两种方法:

定义一个外部变量:

varusergetUserByName('nswbmw').then((_user)=>{user=_userreturngetFollowersByUserId(user._id)}).then((followers)=>{return{user,followers}})

使用闭包:

getUserByName('nswbmw').then((user)=>{returngetFollowersByUserId(user._id).then((followers)=>{return{user,followers}})})

两种实现都可以,但都不太美观。于是我之前产生了一个想法:同一层的 then 的参数是之前所有 then 结果的逆序。体现在代码上就是:

Promise.resolve().then(function(){returngetUserByName('nswbmw')}).then(function(user){returngetFollowersByUserId(user._id)}).then((followers,user)=>{return{user,followers}})

第 3 个 then 的参数是前两个 then 结果的逆序,即 followers 和 user。更复杂比如嵌套 promise 的我就不列了,这种实现的要点在于:如何区分 then 的层级。从 appoint 的实现我们知道,每个 then 返回一个新的 promise,这导致了无法知道当前 then 来自之前嵌套多深的 promise。所以这个想法无法实现。

命名 Promise

后来,我又想出了一种比上面更好的一种解决方法,即命名 Promise:当前 then 的第一个参数仍然是上个 promise 的返回值(即兼容 Promise/A+ 规范),后面的参数使用依赖注入。体现在代码上就是:

Promise.resolve().then(functionuser(){returngetUserByName('nswbmw')}).then(functionfollowers(_,user){returngetFollowersByUserId(user._id)}).then((_,user,followers)=>{return{user,followers}})

上面通过给 then 的回调函数命名(如:user),该回调函数的返回值挂载到 promise 内部变量上(如:values: { user: 'xxx'} ),并把父 promise 的 values 往子 promise 传递。then 的第二个之后的参数通过依赖注入实现注入,这就是命名 Promise 实现的基本思路。我们可以给 Promise 构造函数的参数、then 回调函数和 catch 回调函数命名。

于是,我在 appoint 包基础上修改并发布了 named-appoint 包。

named-appoint 原理:给 promise 添加了 name 和 values 属性,name 是该 promise 的标识(取 Promise 构造函数的参数、then 回调函数或 catch 回调函数的名字),values 是个对象存储了所有祖先 promise 的 name 和 value。当父 promise 状态改变时,设置父 promise 的 value 和 values( this.values[this.name] = value),然后将 values 拷贝到子 promise 的 values,依次往下传递。再看个例子:

constassert=require('assert')constPromise=require('named-appoint')newPromise(functionusername(resolve,reject){setTimeout(()=>{resolve('nswbmw')})}).then(functionuser(_,username){assert(_==='nswbmw')assert(username==='nswbmw')return{name:'nswbmw',age:'17'}}).then(functionfollowers(_,username,user){assert.deepEqual(_,{name:'nswbmw',age:'17'})assert(username==='nswbmw')assert.deepEqual(user,{name:'nswbmw',age:'17'})return[{name:'zhangsan',age:'17'},{name:'lisi',age:'18'}]}).then((_,user,followers,username)=>{assert.deepEqual(_,[{name:'zhangsan',age:'17'},{name:'lisi',age:'18'}])assert(username==='nswbmw')assert.deepEqual(user,{name:'nswbmw',age:'17'})assert.deepEqual(followers,[{name:'zhangsan',age:'17'},{name:'lisi',age:'18'}])}).catch(console.error)

很明显,命名 Promise 有个前提条件是:在同一条 promise 链上。如下代码:

constassert=require('assert')constPromise=require('named-appoint')newPromise(functionusername(resolve,reject){setTimeout(()=>{resolve('nswbmw')})}).then(()=>{returnPromise.resolve().then(functionuser(_,username){assert(username===undefined)return{name:'nswbmw',age:'17'}})}).then(function(_,username,user){assert.deepEqual(_,{name:'nswbmw',age:'17'})assert(username==='nswbmw')assert(user===undefined)}).catch(console.error)

最后一个 then 打印 undefined,因为内部产生了一条新的 promise 链分支。

结合 co 使用

与 co 结合使用是没有什么变化的,如:

constPromise=require('named-appoint')constco=require('co')constpromise=Promise.resolve().then(functionuser(){return'nswbmw'}).then(functionfollowers(){return[{name:'zhangsan'},{name:'lisi'}]}).then((_,user,followers)=>{return{user,followers}})co(function*(){console.log(yieldpromise)/*{user:'nswbmw',followers:[{name:'zhangsan'},{name:'lisi'}]}*/}).catch(console.error)

顺便擅自制定了一个 Promise/A++ 规范。

『挑剔的』错误处理

我们继续脑洞一下。Swift 中错误处理是这样的:

do{trygetFollowers("nswbmw")}catchAccountError.No_User{print("Nouser")}catchAccountError.No_followers{print("Nofollowers")}catch{print("Othererror")}

可以设定 catch 只捕获特定异常的错误,如果之前的 catch 没有捕获错误,那么错误将会被最后那个 catch 捕获。通过命名 catch 回调函数 JavaScript 也可以实现类似的功能,我在 appoint 的基础上修改并发布了 condition-appoint 包。看个例子:

varPromise=require('condition-appoint')Promise.reject(newTypeError('typeerror')).catch(functionSyntaxError(e){console.error('SyntaxError:',e)}).catch(functionTypeError(e){console.error('TypeError:',e)}).catch(function(e){console.error('default:',e)})

将会被第二个 catch 捕获,即打印:

TypeError:[TypeError:typeerror]

修改一下:

varPromise=require('condition-appoint')Promise.reject(newTypeError('typeerror')).catch(functionSyntaxError(e){console.error('SyntaxError:',e)}).catch(functionReferenceError(e){console.error('ReferenceError:',e)}).catch(function(e){console.error('default:',e)})

将会被第三个 catch 捕获,即打印:

default:[TypeError:typeerror]

因为没有对应的错误 catch 函数,所以最终被一个匿名的 catch 捕获。再修改一下:

varPromise=require('condition-appoint')Promise.reject(newTypeError('typeerror')).catch(functionSyntaxError(e){console.error('SyntaxError:',e)}).catch(function(e){console.error('default:',e)}).catch(functionTypeError(e){console.error('TypeError:',e)})

将会被第二个 catch 捕获,即打印:

default:[TypeError:typeerror]

因为提前被匿名的 catch 方法捕获。

condition-appoint 实现原理很简单,就在 appoint 的 then 里加了 3 行代码:

Promise.prototype.then=function(onFulfilled,onRejected){...if(isFunction(onRejected)&&this.state===REJECTED){if(onRejected.name&&((this.value&&this.value.name)!==onRejected.name)){returnthis;}}...};

判断传入的回调函数名和错误名是否相等,不是匿名函数且不相等则通过 return this 跳过这个 catch 语句,即实现值穿透。

当然,condition-appoint 对自定义错误也有效,只要自定义错误设置了 name 属性。