Promise -- 承诺

来源:互联网 发布:aplusvable知乎 编辑:程序博客网 时间:2024/05/17 06:05

1. 链式使用 Promise

现在已经准备好深入 Promise 特性了。首先探索一下如何链式使用 Promsie 。
方法调用的结果:
P.then(onFulfilled, onRejected)
是一个新的 Promise Q 。这意味着你可以通过在 Q 上调用 then() 来维持基于 Promise 的控制流:
• 如果 onFulfilled 或 onRejected 成功返回,则 Q 变为 fulfilled 状态,相应的结果值就是前面函数的返回值。
• 如果 onFulfilled 或 onRejected 抛出异常,则 Q 变为 rejected 状态。

1. 用普通的值将 Q 置为 fulfilled 状态

如果用一个普通的值将 then() 返回的 Promise Q 置为 fulfilled 状态,可以追加一个 then() 来得到那个值:

asyncFunc().then(function (value1) {    return 123;}).then(function (value2) {    console.log(value2); // 123});

2. 用带有 then 方法的对象将 Q 置为 fulfilled 状态

也可以用带有 then 方法的对象 R 将 then() 返回的 Promise Q 置为 fulfilled 状态。可 then 化( thenable )实体就是任何一个有 Promise 风格方法 then() 的对象。因此, Promise 是可 then 化的。用 R 置为 fulfilled 状态(例如在 onFulfilled 中返回 R )意味着 R 被插在 Q “后面”: R 的稳定消息会转发给 Q 的 onFulfilled 和 onRejected 回调。在某种程度上, Q 变成了 R 。

这种机制的主要用途就是扁平化内嵌的 then() 调用,就像下面的例子:

asyncFunc1().then(function (value1) {    asyncFunc2()    .then(function (value2) {        •••    });})扁平化的版本看起来像这样:asyncFunc1().then(function (value1) {    return asyncFunc2();}).then(function (value2) {    •••})

3. 从 onRejected 将 Q 置为 fulfilled 状态

在错误处理器中返回的内容变成了一个成功完成的值(而不是被驳回的值)。这使得可以在执行失败的时候指定默认值:

retrieveFileName().catch(function () {    // Something went wrong, use a default value    return 'Untitled.txt';}).then(function (fileName) {    •••});

4. 通过抛出异常将 Q 置为 rejected 状态

在 then() 的两个回调参数中抛出的异常会传递给下一个错误处理器:

asyncFunc().then(function (value) {    throw new Error();}).catch(function (reason) {    // Handle error here});

5. 执行者中的异常

在执行者( new Promise() 的回调)中抛出的异常传递给该执行者负责管理的 Promise :

new Promise(function (resolve, reject) {    throw new Error();}).catch(function (err) {    // Handle error here});

6. 链式错误

可以有一个或多个 then() 方法调用,并且这些方法调用中都没有传递错误处理器。此时错误就会一直往下传递,直到遇见一个错误处理器。

asyncFunc1().then(asyncFunc2).then(asyncFunc3).catch(function (reason) {    // Something went wrong above});

2. 组合

本节讲述了如何将已有的 Promise 组合起来创建新的 Promise 。我们已经遇到了一种组合 Promise 的方法:通过 then() 依次链式连接。 Promise.all() 和 Promise.race() 提供了另外的组合方式。

1. map() 与 Promise.all() 结合
关于 Promise 一件很好的事情就是很多同步的工具仍然有效,因为基于 Promise 的函数有返回结果。例如,可以使用数组方法 map() :

let fileUrls = [    'http://example.com/file1.txt',    'http://example.com/file2.txt'];let promisedTexts = fileUrls.map(httpGet);

promisedTexts 是一组 Promise 。 Promise.all() 用一个 Promise 数组(可 then 化的和其它值都通过 Promise.resolve() 被转换成了 Promise )作为参数,一旦所有的 Promise 都成功完成了,它就变为 fulfilled 状态,返回结果是这组 Promise 的返回结果的一个数组:

Promise.all(promisedTexts).then(texts => {    for (let text of texts) {        console.log(text);    }}).catch(reason => {    // Receives first rejection among the Promises});

2. 利用 Promise.race() 实现超时

Promise.race() 使用一个 Promise 数组(可 then 化的和其它值都通过 Promise.resolve() 被转换成了 Promise )作为参数,然后返回一个 Promise P 。

传入的 Promise 数组中只要有一个 Promise 变为稳定状态,就会将稳定状态信息传递给返回的那个 Promise P 。
让我们使用 Promise.race() 实现超时:

Promise.race([    httpGet('http://example.com/file.txt'),    delay(5000).then(function () {        throw new Error('Timed out')    });]).then(function (text) { ••• }).catch(function (reason) { ••• });

3. Promise 的优缺点

1. 优点

1.1 统一异步 API

Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。让我们看两个即将到来的基于 Promise 的 API 。
fetch API 是基于 Promise 的,用于处理 XMLHttpRequest 的一种方式:

fetch(url).then(request => request.text()).then(str => •••)

在实际请求中, fetch() 返回一个 Promise ; text() 也返回一个 Promise ,用于将响应内容转换成字符串。
ES6 中可编程地动态引入模块也是基于 Promise 的:

System.import('some_module.js').then(some_module => {    •••})

1.2 Promise 与事件对比

和事件相比较, Promise 更适合处理一次性的结果。

在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。

Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。

1.3 Promise 与回调对比

和回调函数比较, Promise 有更干净的函数(或者方法)签名。回调函数的场景,主函数既有输入参数,又有输出参数:
// name 和 opt 是输入参数, (err, string | Buffer) => void 是输出参数
fs.readFile(name, opts?, (err, string | Buffer) => void)
Promise 的场景,所有的参数都是输入参数:
readFilePromisified(name, opts?) : Promise

4. Promise 内部机制

本节中,我们将会从一个不一样的角度来学习 Promise :我们将会简单实现 Promise ,而不是学习如何使用 Promise API 。这种不同的视角很大程度地帮助我理解了 Promise 。
此处 Promise 的实现叫做 DemoPromise 。为了理解起来更容易,并没有完全符合 ES6 的 Promise API 。但是仍然足以说明实际的 Promise 实现会遇到什么挑战。
DemoPromise 是有三个原型方法的类:
• DemoPromise.prototype.resolve(value)
• DemoPromise.prototype.reject(reason)
• DemoPromise.prototype.then(onFulfilled, onRejected)
也就是说, resolve 和 reject 是方法(比较传递给构造函数 Promise 的回调函数的参数)。

4.1 一个独立的 Promise

第一个实现是一个独立的 Promise ,只有最少的功能:
• 可以创建 Promise
• 可以通过( resolve )或者拒绝( reject )一个 Promise ,并且只能做一次。
• 可以通过 then() 注册应答器(回调函数)。这一定要与 Promise 是否已经稳定无关。
o 该方法暂不支持链式使用 - 不返回任何东西。
下面是如何使用第一个版本的 Promise :
let dp = new DemoPromise();
dp.resolve(‘abc’);
dp.then(function (value) {
console.log(value); // abc
});

下图说明了我们的第一个 DemoPromise 是如何工作的:

这里写图片描述

1. DemoPromise.prototype.then()

首先检查 then() 。该方法处理两种情形:
• 如果 Promise 还处于 pending 状态,则会将 onFulfilled 和 onRejected 的调用存储在队列中,当 Promise 稳定下来的时候就使用。
• 如果 Promise 已经处于 fulfilled 状态或者 rejected 状态, onFulfilled 或者 onRejected 可能会被立刻调用。

then(onFulfilled, onRejected) {    let self = this;    let fulfilledTask = function () {        onFulfilled(self.promiseResult);    };    let rejectedTask = function () {        onRejected(self.promiseResult);    };    switch (this.promiseState) {        case 'pending':            this.fulfillReactions.push(fulfilledTask);            this.rejectReactions.push(rejectedTask);            break;        case 'fulfilled':            addToTaskQueue(fulfilledTask);            break;        case 'rejected':            addToTaskQueue(rejectedTask);            break;    }}

上面的代码片段使用了下面的辅助函数:

function addToTaskQueue(task) {    setTimeout(task, 0);}

2. DemoPromise.prototype.resolve()

resolve() 像下面这样工作:如果 Promise 已经稳定了,什么事也不做(确保 Promise 只能变为稳定状态一次)。

否则, Promise 的状态变为 fulfilled ,结果缓存在 this.promiseResult 中。接下来,触发所有之前存放的 fulfilled 状态相关的回调函数。

resolve(value) {    if (this.promiseState !== 'pending') return;    this.promiseState = 'fulfilled';    this.promiseResult = value;    this._clearAndEnqueueReactions(this.fulfillReactions);    return this; // enable chaining}_clearAndEnqueueReactions(reactions) {    this.fulfillReactions = undefined;    this.rejectReactions = undefined;    reactions.map(addToTaskQueue);}reject() 类似于 resolve() 。

3. 链式

接下来要实现的特性是链式:
• then() 返回一个 Promise ,它通过 onFulfilled 或 onRejected 的返回来变成 resolved 状态。
• 如果省略 onFulfiled 或 onRejected ,那么不管接收到什么,都会传给 then() 返回的 Promise 。

很明显,只需要修改 then() :

then(onFulfilled, onRejected) {    let returnValue = new Promise(); // (A)    let self = this;    let fulfilledTask;    if (typeof onFulfilled === 'function') {        fulfilledTask = function () {            let r = onFulfilled(self.promiseResult);            returnValue.resolve(r); // (B)        };    } else {        fulfilledTask = function () {            returnValue.resolve(self.promiseResult); // (C)        };    }    let rejectedTask;    if (typeof onRejected === 'function') {        rejectedTask = function () {            let r = onRejected(self.promiseResult);            returnValue.resolve(r); // (D)        };    } else {        rejectedTask = function () {            // `onRejected` has not been provided            // => we must pass on the rejection            returnValue.reject(self.promiseResult); // (E)        };    }    •••    return returnValue; // (F)}

then() 创建并返回了一个新的 Promise (行 A 和 F), fulfilledTask 和 rejectedTask 被分别设置:在稳定之后……
• onFulfilled 的返回值传入 returnValue 的 resolve 方法(行 B )。
o 如果没有传入 onFulfilled ,就将当前已经完成的值传入 returnValue 的 resolve 方法(行 C )。
• onRejected 的返回值传入 returnValue 的 resolve 方法(不是 reject 方法!)(行 D )。
o 如果没有传入 onRejected ,就将当前已经完成的值传入 returnValue 的 reject 方法(行 E )。

4. Promise 状态的更多细节

实现链式特性之后, Promise 的状态更加复杂了(正如 ECMAScript 6 规范的25.4节描述的那样):

如果你仅使用 Promise ,通常你可以采用一种简单的方式,忽略掉锁定。最重要的状态相关的概念仍然是“稳定( settledness )”:一个 Promise 在变为 fulfilled 状态或 rejected 状态,就稳定了。

在 Promise 稳定之后,就不再会变化了(状态和最终的值都不会变化)。

如果你想实现 Promise ,那么“解析( resolving )”也很重要,并且现在很难理解:
• 直观地,“ resolved ”的意思是“不能再被(直接地)解析了”。如果一个 Promise 稳定了或者锁定了,那么就变成了 resolved 状态。引用规范里面的话:“一个未解析( unresolved )的 Promise 总是处于 pending 状态。一个解析( resolved )的 Promise 可能是 pending 状态,也可能是 fulfilled 或 rejected 状态”。
• 解析( resolving )不一定会使 Promise 变为稳定状态:你可以用另外一个总是处于 pending 状态的 Promise 来解析某个 Promise 。
• 解析( resolving )现在包括了拒绝( rejecting )(即更一般化了):你可以通过一个处于 rejected 状态的 Promise 来解析某个 Promise ,从而使其也变为 rejected 状态。

5. 异常

最后一个特性,我们希望在用户代码抛出异常的时候进入拒绝( rejection )回调函数。此处,“用户代码”指的是 then() 的两个回调参数。

下面的代码展示了我们如何将 onFulfilled 中抛出的异常转移到拒绝回调函数中 - 通过在行 A 用 try-catch 包裹 onFulfilled 的调用。

then(onFulfilled, onRejected) {    •••    let fulfilledTask;    if (typeof onFulfilled === 'function') {        fulfilledTask = function () {            try {                let r = onFulfilled(self.promiseResult); // (A)                returnValue.resolve(r);            } catch (e) {                returnValue.reject(e);            }        };    } else {        fulfilledTask = function () {            returnValue.resolve(self.promiseResult);        };    }    •••}

1. 启发性的构造器模式

如果我们想将 DemoPromise 变成一个实际可用的 Promise 实现,还需要实现启发性的构造器模式[5]: ES6 的 Promise 不通过方法来解析( resolve )和拒绝( reject ),而是通过传递给执行器( executor )的函数,即构造器的回调参数。

如果执行器抛出异常,那么对应的 Promise 就会变为 rejected 状态。

Promise 附加方法

本节介绍两个很有用的方法,这两个方法可以很容易的添加到 ES6 的 Promise 上面去。很多更全面的 Promise 库都有这两个方法。

1. 扁平化( Flattening )

扁平化主要让链式调用更加方便:通常情况下,将响应器( reaction )返回的值传给下一个 then() 。如果返回一个 Promise ,并且内部并没有做相应的“包装”,这种情形是非常美好的,就像下面的例子:

asyncFunc1().then(function (value1) {    return asyncFunc2(); // (A)}).then(function (value2) {    // value2 is fulfillment value of asyncFunc2() Promise    console.log(value2);});

在行 A 返回一个 Promise ,没必要将下一个 then() 内嵌到当前方法中,可以在当前方法的返回值上调用 then() 。所以:没有内嵌的 then() ,所有内容保持扁平。
通过 resolve() 方法实现这种扁平功能:
• 用 Promise Q 来解析 Promise P 意味着 Q 的稳定情况会转发给 P 。
• P 被“锁在了” Q 上:不能再手动变为 resolved 状态(同样也不能变为 rejected 状态)。它的状态和结果总是和 Q 保持一致。
如果让 Q 变成 thenable 对象(而不仅仅是一个 Promise ),那么就可以让扁平化更加通用,

为了实现锁定,引入一个新的布尔标志 this.alreadyResolved 。一旦该值变为 true , this 就被锁定了,不能再被解析( resolve )了。注意此时 this 可能还处于 pending 状态,因为它的状态现在和锁定到的 Promise 一致。

resolve(value) {    if (this.alreadyResolved) return;    this.alreadyResolved = true;    this._doResolve(value);    return this; // enable chaining}

实际的解析过程现在发生在私有的方法 _doResolve() 中:

_doResolve(value) {    let self = this;    // Is `value` a thenable?    if (typeof value === 'object' && value !== null && 'then' in value) {        // Forward fulfillments and rejections from `value` to `this`.        // Added as a task (vs. done immediately) to preserve async semantics.        addToTaskQueue(function () { // (A)            value.then(                function onFulfilled(result) {                    self._doResolve(result);                },                function onRejected(error) {                    self._doReject(error);                });        });    } else {        this.promiseState = 'fulfilled';        this.promiseResult = value;        this._clearAndEnqueueReactions(this.fulfillReactions);    }}

在行 A 处理了扁平化:如果 value 是 fulfilled 状态,我们希望 self 也是 fulfilled 状态;如果 value 是 rejected 状态,我们希望 self 也是 rejected 状态。通过私有方法 _doResolve 和 _doReject 不停转发,绕过alreadyResolved 标志。

2. done()

当你链接若干个 Promise 方法调用的时候,有一不小心忘记处理某些错误的风险。例如:

function doSomething() {    asyncFunc()    .then(f1)    .catch(r1)    .then(f2); // (A)}

如果在行 A 的 then() 触发了拒绝( rejection ),那么这个错误将不会被处理。 Promise 库 Q 提供了一个方法 done() ,被用作链式调用的最后一个调用元素。 done() 要么替换掉最后一个 then() (此时有一到两个参数):

function doSomething() {    asyncFunc()    .then(f1)    .catch(r1)    .done(f2);}

要么放在最后一个 then() 的后面(此时没有参数):

function doSomething() {    asyncFunc()    .then(f1)    .catch(r1)    .then(f2)    .done();}

Q 的文档:

done 和 then 使用的黄金规则就是:要么返回 Promise 给其它地方,要么调用 done 结束当前 Promise 链(如果这条链在当前位置结束的话)。用 catch 来结束 Promise 是不完美的,因为 catch 的处理器自身可能会抛出错误。
下面就是在 ECMAScript 6 中实现 done() 的方式:

Promise.prototype.done = function (onFulfilled, onRejected) {    this.then(onFulfilled, onRejected)    .catch(function (reason) {        // Throw an exception globally        setTimeout(() => { throw reason }, 0);    });};

虽然 done 的功能很有用,但是还是没有添加到 ECMAScript 6 中去,因为这种检查可以通过引擎自动实现(正如在调试 Promise 的那一节看到的一样)。

3. finally()

有时你想执行一些操作而不管是否发生了错误。例如,在操作完一个资源之后做一些清理工作。这就是 Promise 方法 finally 适用的场景,它和异常处理中的 finally 子句很相似。它的回调函数不接收参数,但是不管是 rejected 还是 resolved 状态都会被调用。

createResource(•••).then(function (value1) {    // Use resource}).then(function (value2) {    // Use resource}).finally(function () {    // Clean up});

下面是 Domenic Denicola 提议的实现 finally() 的方式:

Promise.prototype.finally = function (callback) {    let P = this.constructor;    // We don’t invoke the callback in here,    // because we want then() to handle its exceptions    return this.then(        // Callback fulfills => continue with receiver’s fulfillment or rejection        // Callback rejects => pass on that rejection (then() has no 2nd paramet\er!)        value  => P.resolve(callback()).then(() => value),        reason => P.resolve(callback()).then(() => { throw reason })    );};

回调函数决定了接收器( this )的稳定状态处理的方式:
• 如果回调函数抛出异常或者返回一个 rejected 状态的 Promise ,则会变成拒绝值( rejection value )。
• 否则,接收器的稳定状态( fulfilled 或者 rejected )变成了 finally() 返回的 Promise 的稳定状态。在某种程度上,我们把 finally 从方法链上脱离出来了。

示例1(作者 Jake Archibald )使用 finally() 隐藏一个 spinner 。简单的版本:

showSpinner();fetchGalleryData().then(data => updateGallery(data)).catch(showNoDataError).finally(hideSpinner);

示例2(作者 Kris Kowal )使用 finally() 拆分一个测试:

let HTTP = require("q-io/http");let server = HTTP.Server(app);return server.listen(0).then(function () {    // run test}).finally(server.stop);
原创粉丝点击