深入 Promise(三)——命名 Promise

深入 Promise(三)——命名 Promise

目录:


  1. 深入 Promise(一)——Promise 实现详解
  2. 深入 Promise(二)——进击的 Promise
  3. 深入 Promise(三)——命名 Promise
我们经常会遇到这种情况:比如通过用户名查找并返回该用户信息和他的关注者。通常有两种方法:
  1. 定义一个外部变量:
    var user
    getUserByName('nswbmw')
      .then((_user) => {
        user = _user
        return getFollowersByUserId(user._id)
      })
      .then((followers) => {
        return {
          user,
          followers
        }
      })
    
  2. 使用闭包:
    getUserByName('nswbmw')
      .then((user) => {
        return getFollowersByUserId(user._id).then((followers) => {
          return {
            user,
            followers
          }
        })
      })
    

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

Promise.resolve()
  .then(function () {
    return getUserByName('nswbmw')
  })
  .then(function (user) {
    return getFollowersByUserId(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(function user() {
    return getUserByName('nswbmw')
  })
  .then(function followers(_, user) {
    return getFollowersByUserId(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,依次往下传递。再看个例子:

const assert = require('assert')
const Promise = require('named-appoint')
new Promise(function username(resolve, reject) {
  setTimeout(() => {
    resolve('nswbmw')
  })
})
.then(function user(_, username) {
  assert(_ === 'nswbmw')
  assert(username === 'nswbmw')
  return {
    name: 'nswbmw',
    age: '17'
  }
})
.then(function followers(_, 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 链上。如下代码:

const assert = require('assert')
const Promise = require('named-appoint')
new Promise(function username(resolve, reject) {
  setTimeout(() => {
    resolve('nswbmw')
  })
})
.then(() => {
  return Promise.resolve()
    .then(function user(_, 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 结合使用是没有什么变化的,如:

const Promise = require('named-appoint')
const co = require('co')

const promise = Promise.resolve()
  .then(function user() {
    return 'nswbmw'
  })
  .then(function followers() {
    return [{ name: 'zhangsan' }, { name: 'lisi' }]
  })
  .then((_, user, followers) => {
    return {
      user,
      followers
    }
  })
co(function *() {
  console.log(yield promise)
  /*
  { user: 'nswbmw',
    followers: [ { name: 'zhangsan' }, { name: 'lisi' } ] }
  */
}).catch(console.error)

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

『挑剔的』错误处理

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

do {
  try getFollowers("nswbmw")
} catch AccountError.No_User {
  print("No user")
} catch AccountError.No_followers {
  print("No followers")
} catch {
  print("Other error")
}

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

var Promise = require('condition-appoint')
Promise.reject(new TypeError('type error'))
  .catch(function SyntaxError(e) {
    console.error('SyntaxError: ', e)
  })
  .catch(function TypeError(e) {
    console.error('TypeError: ', e)
  })
  .catch(function (e) {
    console.error('default: ', e)
  })

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

TypeError:  [TypeError: type error]

修改一下:

var Promise = require('condition-appoint')
Promise.reject(new TypeError('type error'))
  .catch(function SyntaxError(e) {
    console.error('SyntaxError: ', e)
  })
  .catch(function ReferenceError(e) {
    console.error('ReferenceError: ', e)
  })
  .catch(function (e) {
    console.error('default: ', e)
  }) 

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

default:  [TypeError: type error]

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

var Promise = require('condition-appoint')
Promise.reject(new TypeError('type error'))
  .catch(function SyntaxError(e) {
    console.error('SyntaxError: ', e)
  })
  .catch(function (e) {
    console.error('default: ', e)
  })
  .catch(function TypeError(e) {
    console.error('TypeError: ', e)
  }) 

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

default:  [TypeError: type error]

因为提前被匿名的 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)) {
      return this;
    }
  }
  ...
};

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

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

编辑于 2017-02-18

文章被以下专栏收录

    这是一家开张已经三年多的酒馆。这酒馆有些特殊,没有一瓶酒,反而吧台上铺满了一堆技术文章?。相传酒馆的老板是一群热爱互联网技术的石墨文档工程师,他们整日挑战不可能完成的任务,为了维护代码之美而奋斗。同时他们也乐于分享,愿意把所得所感散播到世界的各个角落。于是开了家店,名曰技术酒馆?,只因店里面没有一丝酒气,反而弥漫着知识的味道。 对了,他们也秉承着 Open Source by Default 的信念,活跃在 GitHub 上:https://github.com/shimohq。 想找他们聊的话可以发邮件到 join.us@shimo.im。 就酱,比心~