用Promise-A+规范来复盘使用Promise时踩的一个小坑

最近一阵子钻研学习Node.js的服务端代码的写法,看到了各种“Promise大法好啊”、“Promise拯救你于厄运回调金字塔(Pyramid of doom)也就是回调地狱(Callback hell)的水深火热之中”等等云云,作为一个js菜鸟,总想着起点高代码写的好,于是便拍板决定服务端全面采用Promise大法,于是也就分分钟踩了一大堆坑,回头细读了一下Promise/A+的规范,发现其实跟着规范走的话其实还是能躲掉这些坑的,于是本文就使用Promise时可能踩的几个坑进行一下复盘。

首先我们为啥要用Promise

正如前文中所说的,我们使用Promise是为了从回调地狱中解脱出来,什么是回调地狱,可以看下下面这段代码:

doAsync1(function () {
  doAsync2(function () {
    doAsync3(function () {
      doAsync4(function () {
    })
  })
})

由于javascript使用的是非阻塞的异步I/O事件驱动模型,本身引擎是单线程的,所以我们一般会用回调函数的方式来进行异步调用,提高处理器的利用率,一般会写如下的代码:

makeAjaxRequest(url,function(respnose){  
    alert("Response:" + response) ;
}) ;

上述代码实现了一个异步任务,但是如果我们需要在一个异步任务完成以后再进行几个异步任务呢?很简单,在第一个异步任务的回调里套上后续任务的代码块就好了嘛,这就形成了本届开头那种形式的代码,即回调嵌套。
随着业务逻辑变得越来越复杂,你疯狂的使用异步任务,疯狂的使用回调嵌套,然后花式缩进对齐代码,然后就发现你的代码变成了一个“金字塔”形的代码,也就是Pyramid of doom,虽然看起来挺好看的,然而这种代码会越来越难以修改和变更,难以维护。
而在“深入理解 Promise 五部曲”这篇文章中有写到,回调地狱真正根本的问题不是代码缩进一片混乱,难以维护,而是在于由于回调引起的“控制转移”,因为回调里的代码并不是你控制调用的,而是由你调用的那个异步函数“负责”进行调用的,你根本不知道,也没法控制它到底怎么去调用你的callback函数,假设这个异步函数有暗坑,callback不会如你预料的方式被调用,甚至会被调用个十几次,或者不调用,然后你就会面对一个坑爹的系统却丈二摸不着头脑,无能为力。
所以我们引入了Promise来解决这个根本的问题,我们会采用类似于如下的形式来取代回调函数:

function someAsyncThing(){  
    var p = new Promise(function(resolve,reject){
        //at some later time,call 'resolve()' or 'reject()'
    }) ;
    return p ;
}
var p = someAsyncThing() ;  
p.then(  
    function(){
        //success happened    
    },
    function(){
        //failure happened
    }
) ;

在这个例子中,我们不直接将回调函数传递给异步函数,而是让这个异步函数返回了一个Promise对象,然后我们传递给他成功和失败时要进行的操作,当这个异步函数完成的时候,对应的代码块会被执行,就如Promise本身的含义那样,异步函数给了你一个承诺,并会在完成后执行这个承诺。
虽然其实你并没有避开callback函数(then中的函数实质上还是callback),但是通过遵循Promise/A+规范,我们在一定程度上解决了控制转移的问题,让callback函数变得一定程度上可以预测,有一定的规则,当然它也顺便解决了回调金字塔的问题。然而这种使用方式实际上是颠覆了我们正常的callback的思路的,于是这也就意味着我们会踩一些暗坑。

开发中遇到的坑

在开发一个Node.js服务端程序的过程中,我遇到了一连串的Promise串联的问题,我本意想让这些Promise一个一个有序地先后执行,于是我将它们用then串联的起来,然而在调试的过程中发现总有几个Promise没有等之前的Promise执行好就急着要执行,整个程序的逻辑也变得乱七八糟,有些中间传递的参数变成了undefined,当然最终服务端也处于一个崩盘的状态。
在“We have a problem with promises”文章中提出了一个极其类似的踩坑例子,这里我拿过来作为例子使用,我们可以看一下以下的代码:

doSomething().then(function () {
  return doSomethingElse();
}).then(finalHandler);

doSomething().then(function () {
  doSomethingElse();
}).then(finalHandler);

doSomething().then(doSomethingElse())
  .then(finalHandler);

doSomething().then(doSomethingElse)
  .then(finalHandler);

其中doSomething()和doSomethingElse()函数都是返回Promise的异步调用,而finalHandler是一个正常的函数,上述代码块给出了四种用then把它们串起来的方式,实际上这四种不同的方式的表现都各不相同,不过在解释这四种用法的不同前,我们需要先介绍一下几个关于Promise的重要内容和基本原则

Promise/A+规范中的重要内容和几个基本原则

Promise/A+规范的原文位于(https://promisesaplus.com/
中文版的译文可以参考(https://segmentfault.com/a/1190000002452115),这个链接在文章的最后也会予以列出,关于本文没有提及的Promise/A+规范的详细内容可以自行进行参考,本文只拿出几个重要的内容来解释之前的那个问题。
Promise/A+规范中有以下几个重要的基本内容

1.一个Promise必须处在其中之一的状态:pending, fulfilled 或 rejected,pending可以向后两种状态转移,而后两种状态是稳定的,进入之后就永远不会变更,并且会带有一个状态值。

2.规范引入一种叫Promise解析过程的抽象过程,标记为[[Resolve]](promise, x),其详细计算的过程可以参照规范原文,通过引入这个过程,可以将一些非标准的类Promise接口,对象和函数最终规范成一个标准的Promise。

3.通过利用Promise解析过程,then()函数总是能返回一个Promise。

4.then()函数的两个参数如果不是函数,会导致then()函数返回一个与之前promise相同状态的promise

另外还有其他几个相关的基本原则:

1.当你通过new Promise(function (resolve, reject){})的标准形式定义一个Promise时,构造用到的这个函数就已经在被执行了。

2.一个函数在没有返回值时,会默认返回undefined。

有了这些基本的规则,我们可以来看分析前文中给出的四种不同的then串联方式了。

解开坑的谜底

第一种串联方式:

doSomething().then(function () {
  return doSomethingElse();
}).then(finalHandler);

这种串联方式的执行过程类似于下图:

doSomething
|—————–|
       doSomethingElse(undefined)
       |——————|
             finalHandler(resultOfDoSomethingElse)
             |——————|

在这种串联方式中,首先doSomething()执行定义了一个Promise,执行了异步任务,执行完后触发了then的第一个参数的函数,此时doSomethingElse()被不带参数地调用,返回了一个Promise,同时异步任务开始执行,最后当异步任务执行完后,最后一个then被处罚,由于第一个参数直接是finalHandler,所以finalHandler被以doSomethingElse()异步任务的结果值为参数执行。

第二种串联方式

doSomething().then(function () {
  doSomethingElse();
}).then(finalHandler);

这个执行过程如下图:

doSomething
|—————–|
       doSomethingElse(undefined)
       |——————|
       finalHandler(undefined)
       |——————|

这种串联方式和第一种方式中唯一的区别就在于第一个then调用并没有返回对应的doSomethingElse的Promise,这使得这个then函数接受到了一个undefined的返回值,并触发Promise解析过程直接生成了一个fulfilled状态的Promise(结果值为undefined),使得finalHandler提前以undefined为参数执行了,而不会在doSomethingElse异步任务执行之后顺序执行。

第三种串联方式

doSomething().then(doSomethingElse())
  .then(finalHandler);

这个执行过程如下图:

doSomething
|—————–|
doSomethingElse(undefined)
|———————————|
       finalHandler(resultOfDoSomething)
       |——————|

这里的问题有两个:首先doSomethingElse()调用时机和这句串联定义的时机是相同的,而Promise异步任务在定义时就开始执行,这使得doSomethingElse()和doSomething()同时开始执行;其次,then()函数的参数变成了doSomethingElse()的返回值,也就是一个Promise,而不是一个函数,这使得then()函数返回一个和doSomething()返回的Promise相同状态的Promise对象,从而使得finalHandler最后会在doSomething的异步任务执行完后,使用它的结果值来调用。

第四种串联方式

doSomething().then(doSomethingElse)
  .then(finalHandler);

这种执行方式也是比较标准的执行方式,其执行过程如下:

doSomething
|—————–|
       doSomethingElse(resultOfDoSomething)
       |——————|
              finalHandler(resultOfDoSomethingElse)
              |——————|

这里和第一种串联方式的唯一区别在于doSomethingElse函数会以doSomething异步任务的结果值来调用,因为直接将doSomethingElse函数传递给了then函数。

以上就给出了四种串联情况的详细解释,实际过程中如果写出了第二种或者第三种串联形式的代码的话,代码就可能会以出乎你预料的方式执行,让你很难以调试,然而理解了Promise/A+规范的这些基本规则以后,就可以分析出问题在哪里,从而填平这些暗坑了。

参考文章

https://promisesaplus.com/
https://segmentfault.com/a/1190000002452115
http://www.ghostchina.com/promises-the-inversion-problem-part-2/
https://medium.com/@wavded/managing-node-js-callback-hell-1fe03ba8baf#.6vq49tn44
http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
http://liubin.org/promises-book/
https://segmentfault.com/a/1190000002452115

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值