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

术语:决议、完成以及拒绝
为什么单词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 的完成而不对其产生影响。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容