3

0 前言

我一直以为我对Promise比较了解,相关的方法已经非常熟悉了,直到我看到这篇文章,里面提出了这样一个问题:
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四种 promises 的区别是什么

        //1
        doSomething().then(function(){
            return doSomethingElse();
        }).then(finalHandler);
        
        //2
        doSomething().then(function(){
            doSomethingElse();
        }).then(finalHandler);
        
        //3
        doSomething().then(doSomethingElse())
        .then(finalHandler);
        
        //4
        doSomething().then(doSomethingElse)
        .then(finalHandler);

我当时看了是吃惊的,因为我想,这都什么玩意儿!!!所以我把Promise的方法复习了一遍,并且仔细读了上面提到的那篇文章,于是就有了这篇文章。

在前端开发的学习中,新工具层出不穷,理解当前的基础是要理解过去,然后了解将来。就异步调用而言,ES6中引入Promise简化异步操作,主要针对的问题就是回调函数的层层嵌套(金字塔问题),除了阅读不方便之外,只能在当前回调函数函数内部处理异常,这个很难做。Promise通过then和catch方法实现链式调用,每一次调用都返回一个Promise对象,摆脱了回调函数层层嵌套的问题和异步代码“非线性执行”的问题;另外,所有回调函数的报错都可以通过Promise统一处理,catch可以捕获先前调用中所有的异常(冒泡特性)。但是Promise仅仅是对回调做了简化处理,ES7中的async函数更厉害,结合Promise,完全不用回调函数,以近似同步的写法实现异步操作,所需要的仅仅是一个async和await关键字而已。本文仅介绍Promise对象,以及ES6中Promise对象具有的一些操作方法。

1 简单Promise对象

ES6中原生实现了Promise对象,通过状态传递异步操作的消息。Promise对象有三种状态:pending(进行中)、resoleved(fulfilled,已完成)、rejected(已失败),根据异步操作的结果决定处于何种状态。一旦状态改变就不可再变,状态改变仅有两种pending=>rejected、pending=>resolved。
优点:避免了层层嵌套的回调函数,并提供了统一的接口,使异步操作更加容易。
缺点:无法取消Promise;如果不设置回调函数,内部错误无法反映到外部。

1.1 创建Promise实例

Promise构造函数接收两个参数:resolve和reject,这是两个由JavaScript引擎自动提供的函数,不用自己部署。resolve函数在异步操作成功时调用,作用是将Promise对象的状态由pending变为resolved,并将异步操作的结果传递出去。reject函数在异步操作失败时调用,作用是将Promise对象的状态由pending变为reject,将异步操作报错传递出去。
then方法可以接受两个回调函数作为参数,第一个是Pormise对象的状态变为resolved时调用,另一个是当Promise对象的状态变为rejected时调用,这两个回调函数都接受Promise对象实例创建过程中resolve函数和reject函数传出的值作为参数。第二个参数可选,事实上一般通过Promise.prototype.catch()调用发生错误时的回调函数,通过then调用异步操作成功时的回调函数。
实例1:

        //返回Promise对象,setTimeout中传递的resolve参数为’done’
        function timeout(ms) {
          return new Promise((resolve, reject) => {
            setTimeout(resolve, ms, 'done');
          });
        }
        
        timeout(100).then((value) => {
          console.log(value);
        }); //done

实例2:Promise执行流

        //创建Promise实例
        let promise = new Promise(function(resolve, reject) {
          console.log('Promise');//立即执行
          resolve();
        });
        //resolved状态调用在当前脚本所有同步任务执行完才会执行
        promise.then(function() {
          console.log('Resolved.');
        });
        //立即执行
        console.log('Hi!');

对以上代码,Promise新建后立即执行,所以首先输出的是“Promise”。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。

reject函数在异步操作失败时调用,因此参数常常是一个错误对象(Error Object);resolve函数在操作成功时调用,参数常常是正常值或者另一个Promise实例,这表征了异步嵌套,异步嵌套的外层Promise要等待内层Promise的状态决定下一步状态。

        var p1 = new Promise(function (resolve, reject) {
          setTimeout(() => reject(new Error('fail')), 3000)
        })
        
        var p2 = new Promise(function (resolve, reject) {
          setTimeout(() => resolve(p1), 1000)
        })

        p2.then(result => console.log(result)) //p1 is rejected, p2 is the same as p1
          .catch(error => console.log(error)) // Error: fail

由于p2的resolve方法将p1作为参数,p1的状态决定了p2的状态,如果p1的状态是pending,p2的回调函数会等待p1的状态改变;如果p1的状态是resolved或rejected,p2的回调函数立即执行。p2的状态在1秒之后改变,resolve方法返回的是p1。此时,由于p2返回的是另一个Promise,所以后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。

1.2 Promise.prototype.then()

then方法为Promise实例添加状态改变时的回调函数,返回一个新的Promise实例,可以采用链式写法,前一个then方法的返回值作为后一个then方法的参数:

        getJSON("/posts.json").then(function(json) {  // json comes from “/posts.json”
          return json.post;
         }).then(function(post) {  //post comes from json.post
          // ...
        });

如果第一个then方法内的回调函数返回一个Promise对象,后续的then方法会根据这个新的Promise对象的状态执行回调函数。

        getJSON("/post/1.json").then(function(post) {
          return getJSON(post.commentURL);
        }).then(function funcA(comments) {
          console.log("Resolved: ", comments);
        }, function funcB(err){
          console.log("Rejected: ", err);
        });

第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB。回调函数一般是匿名函数,上述仅仅是为了便于理解写成命名函数

1.3 Promise.prototype.catch()

catch(rejection)方法是then(null,rejection)的别称,仅仅当发生错误时执行,catch的存在是将错误回调函数从then()方法中剥离出来。

        getJSON('/posts.json').then(function(posts) {
          // ...
        }).catch(function(error) {
          // 处理 getJSON 和 前一个回调函数运行时发生的错误
          console.log('发生错误!', error);
        });

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

        getJSON('/post/1.json').then(function(post) {
          return getJSON(post.commentURL);
        }).then(function(comments) {
          // some code
        }).catch(function(error) {
          // 处理前面三个Promise产生的错误
        });

catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。要是后续then方法里面报错,就与前面的catch无关了。如果最后一个catch方法内部抛出错误,是无法捕获的。为了避免潜在错误,最好是在最后用一个catch方法兜底。

1.4 Promise.all():偏重状态改变的逻辑与关系

用于将多个Promise实例包装成一个新的Promise实例。如果内部参数不是Promise实例,就调用Promise.resolve()将参数转换为Promise实例。

        var p = Promise.all([p1, p2, p3]);//接受一个Promise数组作为参数

p的状态由p1、p2、p3决定,呈现&关系,fulfilled对应1,rejected对应0,分成两种情况:
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值(rejected的顺序有没有类似与操作的顺序?),会传递给p的回调函数。

        const databasePromise = connectDatabase();
        const booksPromise = databasePromise
          .then(findAllBooks);
        
        const userPromise = databasePromise
          .then(getCurrentUser);
        
        Promise.all([
          booksPromise,
          userPromise
        ])
        .then(([books, user]) => pickTopRecommentations(books, user));

上面代码中,booksPromise和userPromise是两个并行执行的异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommentations这个回调函数。

1.5 Promise.race():偏重状态改变的时间顺序

Promise.race()和Promise.all()同样是将多个Promise实例包装成一个新的Promise实例,但是只要实例数组中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给新实例的回调函数。

        var p = Promise.race([p1, p2, p3]);

Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

        const p = Promise.race([
          fetch('/resource-that-may-take-a-while'),
          new Promise(function (resolve, reject) {
            setTimeout(() => reject(new Error('request timeout')), 5000)
          })
        ]);
        p.then(response => console.log(response));
        p.catch(error => console.log(error));

上面代码中,如果5秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

1.6 Promise.resolve()

将现有对象转化为Promise对象,根据参数不同有不同结果:
(1) 参数是一个Promise实例,Promise.resolve()将原对象返回;
(2) 参数是具有then方法的对象,Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。

        let thenable = {
          then: function(resolve, reject) {
            resolve(42);
          }
        };
        
        let p1 = Promise.resolve(thenable);
        p1.then(function(value) {
          console.log(value);  // 42
        });

thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出42。

(3) 如果参数是一个原始值(基本类型值),或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为Resolved。

        var p = Promise.resolve('Hello');
        p.then(function (s){
          console.log(s)
        });
        // Hello

上面代码生成一个新的Promise对象的实例p。由于字符串Hello不属于异步操作(判断方法是它不是具有then方法的对象),返回Promise实例的状态从一生成就是Resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。
(4) Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。

        var p = Promise.resolve();
        p.then(function () {
          // ...
        });

立即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时,这个很好理解,通过resolve产生的Promise对象然后调用then函数和先产生Promise对象,对象转换成resolved后再执行then函数是一样的,都是在本轮事件轮询的末尾执行。

        //下一轮事件轮询开始
        setTimeout(function () {
          console.log('three');
        }, 0);
        //本轮事件轮询末尾
        Promise.resolve().then(function () {
          console.log('two');
        });
        //立即执行
        console.log('one');
        
        // one two three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(’one‘)则是立即执行,因此最先输出。

1.7 Promise.reject()

Promise.reject()返回一个Promise实例,实例状态为rejected。Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
实例1:

        var p = Promise.reject('出错了');
        // 等同于var p = new Promise((resolve, reject) => reject('出错了'))
        //参数就是’出错了’
        p.then(null, function (s) {
          console.log(s)
        });// 出错了

实例2:

        const thenable = {
          then(resolve, reject) {
            reject('出错了');
          }
        };
        //参数就是thenable
        Promise.reject(thenable)
        .catch(e => {
          console.log(e === thenable)
        })
        // true

上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

2 加深Promise理解

Promises 给予我们的就是在我们使用异步Callback时丢失的最重要的语言基石: return, throw 以及堆栈。但是想要 promises 能够提供这些便利给你的前提是你知道如何正确的使用它们。

2.1 使用Promise.resolve()创建Promise对象

任何有可能 throw 同步异常的代码都是一个后续会导致几乎无法调试异常的潜在因素。但是如果你将所有代码都使用Promise.resolve() 封装,那么你总是可以在之后使用 catch() 来捕获它。因此方法2要优于方法1。

方法1:

new Promise(function(resolve,reject){
    resolve(someSynchronousValue);
}).then(/*-------------*/);

方法2:

function somePromiseAPI() {
    return Promise.resolve().then(function(){
    doSomethinThatMayThrow();
    return ‘foo’;
}).then(/*------------*/);
}

2.2 catch() 与 then(null, ...) 根据使用场景并非完全等价

以下代码等价:

    somePromise().catch(function (err)){
    //handle error
    });
    //////////////////////////////////////
    somePromise().then(null, function(err)) {
    //handle error
    }

但是以下代码不等价:

    somePromise().then(function(){
        return someOtherPromise();
    }).catch(function(err){
        //error
    });
    ///////////////////////////////
    somePromise().then(function(){
        return someOtherPromise();
    },function(err){
        //error
    });

因此,当你使用 then(resolveHandler, rejectHandler) 这种形式时,rejectHandler 并不会捕获由 resolveHandler 引发的异常。最好不使用then()的第二个参数,而是总是使用catch(),唯一例外是写一些异步的Mocha测试时,使用then()的第二个参数,希望抛出用例的异常。

2.3 promises vs promises factories

当我们希望执行一个个的执行一个 promises 序列,即类似 Promise.all() 但是并非并行的执行所有 promises。你可能天真的写下这样的代码:

        function executeSequentially(promise){
            var result = Promise.resolve();
            promises.forEach(function (promise)){
                result = result.then(promise);
            });
            return result;
        }

不幸的是,这份代码不会按照你的期望去执行,你传入 executeSequentially() 的 promises 依然会并行执行。其根源在于你所希望的,实际上根本不是去执行一个 promises 序列。依照 promises 规范,一旦一个 promise 被创建,它就被执行了。因此你实际上需要的是一个 promise factories 数组。
我知道你在想什么:“这是哪个见鬼的 Java 程序猿,他为啥在说 factories?” 。实际上,一个 promises factory 是十分简单的,它仅仅是一个可以返回 promise 的函数:

        function executeSequentially(promiseFactories){
            var result = Promise.resolve();
            promiseFactories,forEach(function (promiseFactory){
            result = result.then(promiseFactory)
        });
        return result;
        }
        function promiseFactory(){
            return somethingThatCreatesAPromise();
        }

为何这样就可以了?这是因为一个 promise factory 在被执行之前并不会创建 promise。它就像一个 then 函数一样,而实际上,它们就是完全一样的东西。如果你查看上面的 executeSequentially() 函数,然后想象 myPromiseFactory 被包裹在 result.then(...) 之中,也许你脑中的小灯泡就会亮起。在此时此刻,对于 promise 你就算是悟道了。

2.4 promises 穿透

        Promise.resolve(‘foo’).then(Promise.resolve(‘bar’)).then(function(result){
        console.log(result);
        });

执行结果并非是bar,而是foo,这是因为当then()接受非函数的参数时,会解释为then(null),这就导致前一个Promise的结果穿透到下面一个Promise。正确的写法是在then()方法内部包含函数:

        Promise.resolve(‘foo’).then(function(){
        return Promise.resolve(‘bar’);
        }).then(function(result){
        console.log(result);
        });

2.5 Promise.all()

Promise.all()以一个Promise数组作为输入,返回一个新的Promise,特点在于它会并行执行数组中的每个Promise,并且每个Promise都返回后才返回结果数组,这就数组的异步版map/forEach方法。但是如果需要返回两个不相关的结果,使用Promise.all()可以产生两个不相关的数组结果;但是如果后一结果要依靠前一个结果产生,此时在Promise里使用嵌套也就可以的:

        getUserByName(‘bill’).then(function(user){
            return getUserAccountById(user.id);
        }).then(function (userAccount){
        /*-----------------*/
        });

或者在内部使用嵌套:

        getUserByName(‘bill’).then(function(user){
                return getUserAccountById(user.id).then(function(userAccount){
                    /*--------------------*/
                });
        });

忘记使用catch:没人可以保证不出错,所以还是在最后加一个catch吧!

Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四种 promises 的区别是什么

        //1
        doSomething().then(function(){
            return doSomethingElse();
        }).then(finalHandler);
        // doSomething()返回一个Promise实例,但是后续的then方法里是一个匿名函数,该函数产生一个新的Promise实例并返回这个实例,因此finalHandler的参数就是这个实例的resolve返回值。
        doSomething
        /-----------------/
                    doSomethingElse(undefined)
                    /----------------------------------/
                                            finalHandler(reulstOfDoSomethingElse)
                                            /--------------------------------------------------/
        //2
        doSomething().then(function(){
            doSomethingElse();
        }).then(finalHandler);
        //doSomething()返回一个Promise实例,但是后续的then方法里是一个匿名函数,该函数产生一个新的Promise实例,但是由于这个函数没有返回值,因此finalHandler函数没有参数。
        doSomething
        /-----------------/
                    doSomethingElse(undefined)
                    /----------------------------------/
                    finalHandler(undefined)
                    /----------------------------------/
        
        //3
        doSomething().then(doSomethingElse())
        .then(finalHandler);
        // doSomething()返回一个Promise实例,但是后续的doSomethingElse()是一个立即执行函数,不接受上一Promise实例的resolve参数,所以参数是undefined,这时doSomething()的Promise穿透到finalHandler,finalHandler的参数就是该Promise的resolve参数。
        doSomething
        /-----------------/
        doSomethingElse(undefined)
        /----------------------------------/
                                finalHandler(reulstOfDoSomething)
                                /--------------------------------------------------/
        
        //4
        doSomething().then(doSomethingElse)
        .then(finalHandler);
        //doSomething()返回一个Promise实例,随后调用doSomethingElse以上一实例的resolve参数产生第二个Promise,最后是finalHandler以上一实例的resolve参数产生第三个Promise。
        doSomething
        /-----------------/
                    doSomethingElse(resultOfDoSomething)
                    /----------------------------------/
                                            finalHandler(reulstOfDoSomethingElse)
                                            /--------------------------------------------------/

zhangding
358 声望23 粉丝

JavaScript+React+Redux