资讯专栏INFORMATION COLUMN

Promise初级与进阶---都在这了

Ocean / 2020人阅读

摘要:处理和前一个回调函数运行时发生的错误发生错误对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。

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)
                                            /--------------------------------------------------/

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81867.html

相关文章

  • 需要知道的JS的日期知识,都在这了

    摘要:实际上是格林威治标准时间的同义词默认情况下,中的几乎每个日期方法除了一个都是本地时间。如果你住在格林威治标准时间晚的的地区,你会得到一个日期是月日。需要知道对象日期方法。 为了保证的可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! JS中的 Date 很奇怪。当我们需要处理日期和时间的时候比较麻烦,经常借助像date-fns和 Mom...

    testbird 评论0 收藏0
  • 你要的 React 面试知识点,都在这了

    摘要:是流行的框架之一,在年及以后将会更加流行。于年首次发布,多年来广受欢迎。下面是另一个名为的高阶函数示例,该函数接受另外两个函数,分别是和。将所有较小的函数组合成更大的函数,最终,得到一个应用程序,这称为组合。 React是流行的javascript框架之一,在2019年及以后将会更加流行。React于2013年首次发布,多年来广受欢迎。它是一个声明性的、基于组件的、用于构建用户界面的高...

    klinson 评论0 收藏0
  • 零基础 | 入行软件测试,你想知道的在这里了

    摘要:应届生零基础可以学习软件测试吗俗话说,人怕入错行。霸哥这里分别从入行难度入行方式行业前景薪资待遇四个方面来分析一下。目前市场上的测试人员,一部分是企业自己培养的,另一部分是来自培训机构。软件测试的行业门槛低,市场需求量大。 ...

    neroneroffy 评论0 收藏0
  • 前端进阶系列-目录

    摘要:然而在最近的面试中通过学习和思考,找到了前进的方向,也得到一些大公司的录用机会。算是从初级前端毕业,进阶了吧。在这里先写个目录。赶时间的同学可以按照我的目录先自行准备提升,希望推荐文章和交流。 背景 之前我分享了文章大厂前端面试考什么?,你们一定很想看答案吧?说实话,答案我是有,在准备面试的时候会时不时翻看,但内容比较多,比较凌乱,不能指望我在一篇文章中写完。 我是从非计算机专业自学前...

    cod7ce 评论0 收藏0
  • 编写一个并发性能测试的小程序

    摘要:并发线程测试循环新建线程类,并在线程体内塞入单个的测试用例,以及全局的计数类。为了让性能测试更充分,我编写了不同的计算过程,并使用随机函数随机获取并塞入线程执行。 ...

    Michael_Lin 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<