V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
1heart
V2EX  ›  分享创造

深入 Promise(三)——命名 Promise

  •  
  •   1heart · 2017-02-15 10:28:46 +08:00 · 2681 次点击
    这是一个创建于 2842 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们经常会遇到这种情况:比如通过用户名查找并返回该用户信息和他的关注者。通常有两种方法: 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 ,依次往下传递。再看个例子:

    var Promise = require('named-appoint')
    new Promise(function username(resolve, reject) {
      setTimeout(() => {
        resolve('nswbmw')
      })
    })
    .then(function user(_, username) {
      return {
        name: 'nswbmw',
        age: '17'
      }
    })
    .then(function followers(_, username, user) {
      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' } ])
    })
    

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

    new Promise(function username(resolve, reject) {
      setTimeout(() => {
        resolve('nswbmw')
      })
    })
    .then(() => {
      return Promise.resolve()
        .then(function user(_, username) {
          console.log(username)// undefined
          return {
            name: 'nswbmw',
            age: '17'
          }
        })
    })
    .then(function (_, username, user) {
      assert.deepEqual(_, { name: 'nswbmw', age: '17' })
      assert(username === 'nswbmw')
      assert.deepEqual(user, { name: 'nswbmw', age: '17' })
    })
    

    打印 undefined ,因为内部产生了一条新的 promise 链分支。后面的 3 个 assert 都通过,因为整体上是一条 promise 链。

    顺便擅自制定了一个 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 捕获。通过命名回调函数 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 属性。

    12 条回复    2017-02-17 11:19:43 +08:00
    leonlu
        1
    leonlu  
       2017-02-15 10:57:17 +08:00
    完全没有 async/await 简单吧。。。
    ericls
        2
    ericls  
       2017-02-15 11:01:13 +08:00
    @leonlu async/await 是最差的方式

    因为看起来的东西和实际上的东西不一样
    aleung
        3
    aleung  
       2017-02-15 13:12:16 +08:00 via Android
    @ericls 为何这样说?能否具体解释一下
    ericls
        4
    ericls  
       2017-02-15 13:17:30 +08:00 via iPhone
    @aleung async 和 await 看起来像 sync 的吧?

    但是实际上是 event loop 吧??

    所以你到底是用哪种思路去思考呢?
    aleung
        5
    aleung  
       2017-02-15 13:38:06 +08:00 via Android
    @ericls 不知道是不是过去经验不同引起的思维方式区别,我觉得 await 大大降低了异步代码的理解难度,一看到 await 就知道这里不会马上返回,而是“阻塞”在这里等有“结果”了才会继续下去。程序就像多线程执行一样。
    ericls
        6
    ericls  
       2017-02-15 13:52:01 +08:00 via iPhone
    @aleung 你都说了就像……
    ericls
        7
    ericls  
       2017-02-15 13:59:51 +08:00
    @aleung 或者应该说对 callstack eventloop 有了了解之后再写 async/await. 不然那就是自己骗自己。。。
    hronro
        8
    hronro  
       2017-02-15 14:02:02 +08:00 via Android
    二维码呢😂
    aleung
        9
    aleung  
       2017-02-15 15:32:27 +08:00 via Android
    @ericls 那是的,什么东西都要知其然也知其所以然才能用得好。
    arzusyume
        10
    arzusyume  
       2017-02-17 08:51:55 +08:00
    @ericls 不觉得 async 很差劲, 没 async 之前还不是一堆人用 Q...
    说白了, async 本身就是嫌弃 Promise 写起来太麻烦才封装的一个 generator
    至于"看着和想的不一样", 那就要看是从什么角度上去思考了, 就代码本身表达而言个人认为 async/await 的代码读起来更容易理解... 虽然细思或许 Promise 的逻辑更加清晰
    leonlu
        11
    leonlu  
       2017-02-17 11:12:13 +08:00
    LZ 要解决的这个问题从根本上讲是 resolve 和 reject 只能接受一个参数导致的。而这么设计其中的一个原因是 chaining 这个事儿。因为 chaining 中的 return 只能返回一个参数。(另外大神说 es2015 中已经基本没有 API 是可变参数了,这也是 JS 的规范趋势。)

    所以,要我瞎搞,我觉得应该有一个 return Promise.Result(a, b, c, d ...);这样的话就可以:

    new Promise(resolve => resolve(a, b, c, d))
    .then((a, b, c, d) => return Promise.Result(b, c, d))
    .then((b, c, d) => {});

    当然还有各种细节,比如 Result 的参数可不可以是 promise 等等,瞎搞就不深入了。
    leonlu
        12
    leonlu  
       2017-02-17 11:19:43 +08:00
    PS : async / await 是不可抗拒的,规范也有了,而且 V8 / FF 都实现了啊。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5490 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:05 · PVG 16:05 · LAX 00:05 · JFK 03:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.