美文网首页
你不知道的JavaScript(中卷)|Promise(二)

你不知道的JavaScript(中卷)|Promise(二)

作者: xpwei | 来源:发表于2018-04-19 11:52 被阅读22次

    术语:决议、完成以及拒绝
    为什么单词resolve(比如在Promise.resolve(..)中)如果用于表达结果可能是完成也可能是拒绝的话,既没有歧义,并且也确实更精确:

    var rejectedTh = {
        then: function (resolved, rejected) {
            rejected("Oops");
        }
    };
    var rejectedPr = Promise.resolve(rejectedTh);
    

    前面已经介绍过,Promise.resolve(..)会将传入的真正Promise直接返回,对传入的thenable则会展开。如果这个thenable展开得到一个拒绝状态,那么从Promise.resolve(..)返回的Promise实际上就是这同一个拒绝状态。
    Promise(..)构造器的第一个参数回调会展开thenable(和Promise.resolve(..)一样)或真正的Promise:

    var rejectedPr = new Promise(function (resolve, reject) {
        // 用一个被拒绝的promise完成这个promise
        resolve(Promise.reject("Oops"));
    });
    rejectedPr.then(
        function fulfilled() {
            // 永远不会到达这里
        },
        function rejected(err) {
            console.log(err); // "Oops"
        }
    );
    

    现在应该很清楚了,Promise(..)构造器的第一个回调参数的恰当称谓是resolve(..)。

    前面提到的reject(..)不会像resolve(..)一样进行展开。如果向reject(..)传入一个Promise/thenable值,它会把这个值原封不动地设置为拒绝理由。后续的拒绝处理函数接受到的是你实际传给reject(..)的那个Promise/thenable,而不是其低层的立即值。

    不过,现在我们再来关注一下提供给then(..)的回调。它们(在文献和代码中)应该怎么命名呢?我的建议是fullfilled(..)和reject(..)。

    function fulfilled(msg) {
        console.log(msg);
    }
    function rejected(err) {
        console.error(err);
    }
    p.then(
        fulfilled,
        rejected
    );
    

    对then(..)的第一个参数来说,毫无疑义,总是处理完成的情况,所以不需要使用标识两种状态的术语“resolve”。这里提一下,ES6规范将这两个回调命名为onFulfilled(..)和onRjected(..),所以这两个术语很准确。

    错误处理
    对多数开发者来说,错误处理最自然的形式就是同步的try..catch构造。遗憾的是,它只能是同步的,无法用于异步代码模式:

    function foo() {
        setTimeout(function () {
            baz.bar();
        }, 100);
    }
    try {
        foo();
        // 后面从 `baz.bar()` 抛出全局错误
    }
    catch (err) {
        // 永远不会到达这里
    }
    

    try..catch当然很好,但是无法跨异步操作工作。也就是说,还需要一些额外的环境支持。
    在回调中,一些模式化的错误处理方式已经出现,最值得一提的是error-first回调风格:

    function foo(cb) {
        setTimeout(function () {
            try {
                cb(null, 1); // 成功!
            }
            catch (err) {
                cb(err);
            }
        }, 100);
    }
    foo(function (err, val) {
        if (err) {
            console.error(err); // 烦 :(
        }
        else {
            console.log(val);//1
        }
    });
    

    只有在baz.bar()调用会同步地立即成功或失败的情况下,这里的try..catch才能工作。如果baz.bar()本身有自己的异步完成函数,其中的任何异步错误都将无法捕捉到。

    传给foo(..)的回调函数保留第一个参数err,用于在出错时接受到信号。如果其存在的话,就认为出错,否则就认为是成功。
    严格来说,这一类错误处理是支持异步的,但完全无法很好地组合。多级error-first回调交织在一起,再加上这些无所不在的if检查语句,都不可避免地导致了回调地狱的风险。

    var p = Promise.resolve(42);
    p.then(
        function fulfilled(msg) {
            // 数字没有string函数,所以会抛出错误
            console.log(msg.toLowerCase());
        },
        function rejected(err) {
            // 永远不会到达这里
        }
    );
    

    如果msg.toLowerCase()合法地抛出一个错误(事实确实如此!),为什么我们的错误处理函数没有得到通知呢?正如前面解释过,这是因为那个错误处理函数是为promise p准备的,而这个promise已经用值42填充了。promise p是不可变的,所以唯一可以被通知这个错误的promise是从p.then(..)返回的那一个,但我们在此例中没有捕捉。
    这应该清晰地解释了为什么Promise的错误处理易于出错。这非常容易造成错误被吞掉,而这极少是出于你的本意。

    绝望的陷阱
    毫无疑问,Promise错误处理就是一个“绝望的陷阱”设计。默认情况下,它假定你想要Promise状态吞掉所有的错误。如果你忘了查看这个状态,这个错误就会默默地(通常是绝望地)在暗处凋零死掉。
    为了避免丢失被忽略和抛弃的Promise错误,一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(..)结束:

    var p = Promise.resolve(42);
    p.then(
        function fulfilled(msg) {
            // 数字没有string函数,所以会抛出错误
            console.log(msg.toLowerCase());
        }
    )
        .catch(handleErrors);
    

    因为我们没有为then(..)传入拒绝处理函数,所以默认的处理函数被替换掉了,而这仅仅是把错误传递给了链中的下一个promise。因此,进入p的错误以及p之后进入其决议(就像msg.toLowerCase())的错误都会传递到最后的handleErrors(..)。
    如果handleErrors(..)本身内部也有错误怎么办呢?谁来捕捉它?还有一个没人处理的promise:catch(..)返回的那一个。我们没有捕获这个promise的结果,也没有为其注册拒绝处理函数。
    你并不能简单地在这个链尾端添加一个新的catch(..),因为它很可能会失败。任何Promise链的最后一步,不管是什么,总是存在着在未被查看的Promise中出现未捕获错误的可能性,尽管这种可能性越来越低。

    处理未捕获的情况
    Promsie 应该添加一个done(..) 函数,从本质上标识Promsie 链的结束。done(..) 不会创建和返回Promise,所以传递给done(..) 的回调显然不会报告一个并不存在的链接Promise 的问题。
    那么会发生什么呢?它的处理方式类似于你可能对未捕获错误通常期望的处理方式:done(..) 拒绝处理函数内部的任何异常都会被作为一个全局未处理错误抛出(基本上是在开发者终端上)。代码如下:

    var p = Promise.resolve(42);
    p.then(function fulfilled(msg) {
        // 数字没有string函数,所以会抛出错误
        console.log(msg.toLowerCase());
    }).done(null, handleErrors);
    // 如果handleErrors(..)引发了自身的异常,会被全局抛出到这里
    

    相比没有结束的链接或者任意时长的定时器,这种方案看起来似乎更有吸引力。但最大的问题是,它并不是ES6 标准的一部分,所以不管听起来怎么好,要成为可靠的普遍解决方案,它还有很长一段路要走。
    浏览器有一个特有的功能是我们的代码所没有的:它们可以跟踪并了解所有对象被丢弃以及被垃圾回收的时机。所以,浏览器可以追踪Promise 对象。如果在它被垃圾回收的时候其中有拒绝,浏览器就能够确保这是一个真正的未捕获错误,进而可以确定应该将其报告到开发者终端。

    Promise 模式
    Promise.all([..])
    在异步序列中(Promise 链),任意时刻都只能有一个异步任务正在执行——步骤2 只能在步骤1 之后,步骤3 只能在步骤2 之后。但是,如果想要同时执行两个或更多步骤(也就是“并行执行”),要怎么实现呢?
    在经典的编程术语中,门(gate)是这样一种机制要等待两个或更多并行/ 并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。
    在Promise API 中,这种模式被称为all([ .. ])。

    // request(..)是一个Promise-aware Ajax工具
    // 就像我们在本章前面定义的一样
    var p1 = request("http://some.url.1/");
    var p2 = request("http://some.url.2/");
    Promise.all([p1, p2]).then(function(msgs) {
        // 这里,p1和p2完成并把它们的消息传入
        return request("http://some.url.3/?v=" + msgs.join(","));
    }).then(function(msg) {
        console.log(msg);
    });
    

    从Promise.all([ .. ]) 返回的主promise 在且仅在所有的成员promise 都完成后才会完成。如果这些promise 中有任何一个被拒绝的话,主Promise.all([ .. ])promise 就会立即被拒绝,并丢弃来自其他所有promise 的全部结果。

    Promise.race([..])
    尽管Promise.all([ .. ]) 协调多个并发Promise 的运行,并假定所有Promise 都需要完成,但有时候你会想只响应“第一个跨过终点线的Promise”,而抛弃其他Promise。
    这种模式传统上称为门闩,但在Promise 中称为竞态。
    Promise.race([ .. ]) 也接受单个数组参数。这个数组由一个或多个Promise、thenable 或立即值组成。立即值之间的竞争在实践中没有太大意义,因为显然列表中的第一个会获胜,就像赛跑中有一个选手是从终点开始比赛一样!
    与Promise.all([ .. ]) 类似,一旦有任何一个Promise 决议为完成,Promise.race([ .. ])就会完成;一旦有任何一个Promise 决议为拒绝,它就会拒绝。

    // request(..)是一个支持Promise的Ajax工具
    // 就像我们在本章前面定义的一样
    var p1 = request("http://some.url.1/");
    var p2 = request("http://some.url.2/");
    Promise.race([p1, p2]).then(function(msg) {
        // p1或者p2将赢得这场竞赛
        return request("http://some.url.3/?v=" + msg);
    }).then(function(msg) {
        console.log(msg);
    });
    

    因为只有一个promise 能够取胜,所以完成值是单个消息,而不是像对Promise.all([ .. ])那样的是一个数组。

    1. 超时竞赛
      我们之前看到过这个例子,其展示了如何使用Promise.race([ .. ]) 表达Promise 超时模式:
    // foo()是一个支持Promise的函数
    // 前面定义的timeoutPromise(..)返回一个promise,
    // 这个promise会在指定延时之后拒绝
    // 为foo()设定超时
    Promise.race([
        foo(), // 启动foo()
        timeoutPromise(3000) // 给它3秒钟
    ]).then(function() {
        // foo(..)按时完成!
    }, function(err) {
        // 要么foo()被拒绝,要么只是没能够按时完成,
        // 因此要查看err了解具体原因
    });
    

    在多数情况下,这个超时模式能够很好地工作。但是,还有一些微妙的情况需要考虑,并且坦白地说,对于Promise.race([ .. ]) 和Promise.all([ .. ]) 也都是如此。

    1. finally
      它看起来可能类似于:
    var p = Promise.resolve( 42 );
    p.then( something )
    .finally( cleanup )
    .then( another )
    .finally( cleanup );
    

    同时,我们可以构建一个静态辅助工具来支持查看(而不影响)Promise 的决议:

    // polyfill安全的guard检查
    if (!Promise.observe) {
        Promise.observe = function(pr, cb) {
            // 观察pr的决议
            pr.then(function fulfilled(msg) {
                // 安排异步回调(作为Job)
                Promise.resolve(msg).then(cb);
            }, function rejected(err) {
                // 安排异步回调(作为Job)
                Promise.resolve(err).then(cb);
            });
            // 返回最初的promise
            return pr;
        };
    }
    

    下面是如何在前面的超时例子中使用这个工具:

    Promise.race([
        Promise.observe(foo(), // 试着运行foo()
            function cleanup(msg) {
                // 在foo()之后清理,即使它没有在超时之前完成
            }),
        timeoutPromise(3000) // 给它3秒钟
    ])
    

    这个辅助工具Promise.observe(..) 只是用来展示可以如何查看Promise 的完成而不对其产生影响。

    相关文章

      网友评论

          本文标题:你不知道的JavaScript(中卷)|Promise(二)

          本文链接:https://www.haomeiwen.com/subject/pxfzvxtx.html