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

深入 Promise(三)命名 Promise

  •  
  •   1heart 2017 年 2 月 15 日 3268 次点击
    这是一个创建于 3339 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我们经常会遇到这种情况:比如通过用户名查找并返回该用户信息和他的关注者。通常有两种方法: 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 年 2 月 15 日
    完全没有 async/await 简单吧。。。
    ericls
       2
    ericls  
       2017 年 2 月 15 日
    @leonlu async/await 是最差的方式

    因为看起来的东西和实际上的东西不一样
    aleung
        3
    aleung  
       2017 年 2 月 15 日 via Android
    @ericls 为何这样说?能否具体解释一下
    ericls
        4
    ericls  
       2017 年 2 月 15 日 via iPhone
    @aleung async 和 await 看起来像 sync 的吧?

    但是实际上是 event loop 吧??

    所以你到底是用哪种思路去思考呢?
    aleung
        5
    aleung  
       2017 年 2 月 15 日 via Android
    @ericls 不知道是不是过去经验不同引起的思维方式区别,我觉得 await 大大降低了异步代码的理解难度,一看到 await 就知道这里不会马上返回,而是“阻塞”在这里等有“结果”了才会继续下去。程序就像多线程执行一样。
    ericls
        6
    ericls  
       2017 年 2 月 15 日 via iPhone
    @aleung 你都说了就像……
    ericls
        7
    ericls  
       2017 年 2 月 15 日
    @aleung 或者应该说对 callstack eventloop 有了了解之后再写 async/await. 不然那就是自己骗自己。。。
    hronro
        8
    hronro  
       2017 年 2 月 15 日 via Android
    二维码呢
    aleung
        9
    aleung  
       2017 年 2 月 15 日 via Android
    @ericls 那是的,什么东西都要知其然也知其所以然才能用得好。
    arzusyume
        10
    arzusyume  
       2017 年 2 月 17 日
    @ericls 不觉得 async 很差劲, 没 async 之前还不是一堆人用 Q...
    说白了, async 本身就是嫌弃 Promise 写起来太麻烦才封装的一个 generator
    至于"看着和想的不一样", 那就要看是从什么角度上去思考了, 就代码本身表达而言个人认为 async/await 的代码读起来更容易理解... 虽然细思或许 Promise 的逻辑更加清晰
    leonlu
        11
    leonlu  
       2017 年 2 月 17 日
    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 年 2 月 17 日
    PS : async / await 是不可抗拒的,规范也有了,而且 V8 / FF 都实现了啊。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5567 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 42ms UTC 09:32 PVG 17:32 LAX 02:32 JFK 05:32
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86