全面理解 Promise 系列(二):链式 Promise
小举 筑基

到目前为止, Promise 可能看起来只不过是结合回调函数和 setTimeout() 所做的渐进式改进,但其意义远比表面看起来丰富的多。具体地说,我们可以用多种方法将 Promise 链接起来,以实现更复杂的异步功能。

实际上,对then()catch()finally()的每次调用都会创建并返回另一个 Promise。只有在第一个 Promise 被履行或者被拒绝之后,第二个 Promise 的状态才能确定。参考以下例子:

1
2
3
4
5
6
7
8
9
10
11
const promise = Promise.resolve(42);

promise
.then((value) => {
console.log(value); // 42
})
.then(() => {
console.log('Finished');
});
// 42
// Finished

调用 promise.then() 会返回第 2 个 Promise, 并在这个新的 Promise 上再次调用 then() 。 只有在第 1 个 Promise 被解决之后,第 2 个 then() 里的履行处理器才会被调用。如果将本例解链,就会得到如下的代码:

1
2
3
4
5
6
7
const promise1 = Promise.resolve(42);
const promise2 = promise1.then((value) => {
console.log(value);
});
promise2.then(() => {
console.log('Finished');
});

在这段非链式版本的代码中, promise1.then() 的结果被存储在 promise2 中,然后调用promise2.then() 来添加最后的履行处理器。对 promise2.then() 的调用也会返回一个新的 Promise,只不过我们没有用它。

2.1 捕获错误

使用链式 Promise 有利于捕获之前 Promise 中的履行器或者拒绝处理器所产生的错误。来看一个例子:

1
2
3
4
5
6
7
8
const promise = Promise.resolve(42);
promise
.then(() => {
throw new Error('Oops');
})
.catch((reason) => {
console.error(reason.message); // "Oops"
});

在这段代码中, promise 的履行处理器抛出了一个错误。在第 2 个 Promise 上对 catch() 的链式调用,能够通过拒绝处理器接受该错误。如果是拒绝处理器抛出错误,那么情况也一样:

1
2
3
4
5
6
7
8
9
10
11
const promise = new promise((resolve, reject) => {
throw new Error('Uh oh!');
});
promise
.catch((reason) => {
console.log(reason.message); // "Uh oh!"
throw new Error('Oops');
})
.catch((reason) => {
console.error(reason.message); // "Oops"
});

执行器抛出的错误出发了拒绝处理器。然后,拒绝处理器又抛出了一个新的错误,这个错误被第 2 个 Promise 的拒绝处理器捕获。链式 Promise 中拒绝处理器的调用能够感知链中其他 Promise 抛出的错误。

我们可以利用链式 Promise 的这种能力来捕获错误,使之有效的起到类似于 try-catch 语句的作用。假如使用 fetch() 来获取一些数据,并希望捕获发生的错误:

1
2
3
4
5
6
7
8
9
10
11
const promise = fetch('book.json');

promise
.then((response) => {
// 处理成功的响应
console.log(response.status);
})
.catch((reason) => {
// 处理失败的原因
console.error(reason.message);
});

这个例子将在 fetch() 调用成功时输出相应状态,在调用失败的时候输出错误消息。我们可以进一步通过检查 resposne.ok 属性来将范围 200 ~ 299 之外的状态码作为错误抛出,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const promise = fetch('book.json');

promise
.then((response) => {
if (response.ok) {
// 处理成功的响应
console.log(response.status);
} else {
// 将错误抛出
throw new Error(
`Request failed with status code ${response.status} ${response.statusText}`
);
}
})
.catch((reason) => {
// 处理失败的原因
console.error(reason.message);
});

在本例中,对 catch() 的调用注册了一个拒绝处理器。该拒绝处理器既可以捕获 fetch() 调用返回的错误,也可以捕获第一个履行处理器抛出的错误。 也就是说,我们可以仅仅使用一个拒绝处理器来处理链式 Promise 中可能发生的所有错误,而无需使用两个处理器来捕获不同类型的错误。

💡 始终在链式 Promise 的末端添加一个拒绝处理器。这样做可以确保正确的处理链式 Promise 中可能发生的任何错误。

2.2 在链式 Promise 中使用 finally()

finally 的表现与 then()catch() 不同。它将前一个 Promise 的状态和值复制到其返回的 Promise 中。 这意味着如果原来的 Promise 处于履行状态,那么 finally() 会返回一个同样处于履行状态且值相同的 Promise。 来看一个例子:

1
2
3
4
5
6
7
8
9
const promise = Promise.resolve(42);

promise
.finally(() => {
console.log('Finally called.');
})
.then((value) => {
console.log(value); // 42
});

在这个例子中,因为 finally() 注册的处理器不接受参数,无法接受 promise 的值,所以该值被复制到由于调用 finally() 而新创建的 Promise 中。 新的 Promise 履行的值是 42(从原 Promise 复制过来),因此 then() 处理器被调用,接受 42 作为参数。需要注意的是,即使新的 Promise 和 原 Promise 有着相同的值,它们并不是同一个对象,如下所示:

1
2
3
4
5
6
7
8
9
const promise1 = Promise.resolve(42);
const promise2 = promise1.finally(() => {
console.log('Finally called.');
});
promise2.then((value) => {
console.log(value); // 42
});

console.log(promise1 === promise2); // false

在这段代码中,从 promise1.finally() 中返回的值被存储在 promise2 中。由此可知, promise1 和 promise2 并不是同一个对象。finally() 的调用总是从原 Promise 复制状态和值。这也意味着,当 finally() 在一个被拒绝的 Promise 上被调用时,它也会返回一个处于拒绝状态的 Promise。如下所示:

1
2
3
4
5
6
7
8
9
const promise = Promise.reject(43);

promise
.finally(() => {
console.log('Finally called!');
})
.catch((reason) => {
console.error(reason); // 43
});

这个例子中的 promise 处于拒绝状态,且拒绝理由是 43。和之前的例子一样,解决处理器无法获取该信息,因为 finally() 没有可以传入的参数。因此它返回了一个新的 Promise,且该 Promise 因同样的理由被拒绝。之后我们可以通过使用 catch() 来检索该 Promise 被拒绝的理由。

当解决处理器抛出错误或者返回处于拒绝状态的 Promise 时,finally() 存在一个例外情况。在这种情况下,finally() 返回的 Promise 并不保持原 Promise 的状态和值。,而是以抛出的错误为理由变为拒状态,来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promise1 = Promise.reject(43);
promise1
.finally(() => {
throw 44;
})
.catch((reason) => {
console.error(reason); // 44 而不是 43
});

const promise2 = Promise.resolve(43);
promise2
.finally(() => {
return Promise.reject(44);
})
.catch((reason) => {
console.error(reason); // 44 而不是 43
});

在这例子中,因为解决处理器抛出 44 或者返回了 promise.reject(44) ,所以被返回的 Promise 处于拒绝状态,并且其值为 44 而不是 43。由于解决处理器抛出了错误,因此原 Promise 的状态和值就丢失了。

在系列(一)中,我们举例了如何使用解决处理器根据对 fetch() 的调用来切换应用程序的加载状态。现在我们使用链式 Promise 重写这个例子,并加入一些错误处理技巧, 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const appElement = document.getElementById('app');
const promise = fetch('book.json');

appElement.classList.add('loading');

promise
.then((response) => {
if (response.ok) {
console.log(response.status);
} else {
throw new Error(
`Request failed with status code ${response.status} ${response.statusText}`
);
}
})
.finally(() => {
appElement.classList.remove('loading');
})
.catch((reason) => {
console.error(reason.message);
});

与 try-catch 语句不同,我们不希望 finally() 成为链式 Promise 的最后一环,以防 finally() 抛出错误。因此,应该首先调用then(),以处理来自fetch()的响应,然后在链中添加finally()来触发 UI 变化,最后通过catch()来添加整条链的错误处理器。 这里体现了解决处理器传递前一个 Promise 的状态的用处:如果履行处理器最终抛出一个错误,那么解决处理器(finally())将把该状态进一步传递给catch(),以便拒绝处理器访问该状态。

2.3 从链式 Promise 返回值

链式 Promise 的另外一个重要的能力是将数据从一个 Promise 传递给下一个 Promise。我们已经知道,执行器(executor)内部如果调用 resolve(), 传递个 resolve 的参数会被传递给该 Promise 的履行处理器。我们可以通过指定履行处理器的返回值来继续沿着链式 Promise 传递数据,如下所示:

1
2
3
4
5
6
7
8
9
10
const promise = Promise.resolve(42);

promise
.then((value) => {
console.log(value); // 42
return value + 1; // 我们显式的返回一个值
})
.then((value) => {
console.log(value); // 43
});

Promise 的履行执行器在执行的时候返回了 value + 1。虽然 value 的值是 42(来自执行器),但是该履行处理器实际上返回了 43。然后这个值被继续传递给第二个 Promise 的履行处理器。
我们可以使用拒绝处理器完成同样的工作。拒绝处理器在被执行的时候也可以显式的返回一个值。如果是这样,那么这个值就会传递给履行链中的下一个 Promise, 如下所示:

1
2
3
4
5
6
7
8
9
10
const promise = Promise.reject(42);

promise
.catch((reason) => {
console.error(reason); // 42
return value + 1; // 我们显式的返回一个值
})
.then((value) => {
console.log(value); // 43
});

我们在此创建了一个处于拒绝状态且值为 42 的 Promise,这个值被传递给该 Promise 的拒绝处理器 拒绝处理器将返回 value + 1,虽然这个返回值来自拒绝处理器,但是它仍可被用于链中下一个 Promise 的履行处理器,如有必要,在链中的某一个 Promise 失败后,我们能够利用这一特点来恢复整条 Promise 链的操作。然而,使用 finally()会得到不同的结果,从解决处理器返回的任何值都会被忽略,这样一来,我们就可以访问原 Promise 的值。来看以下例子:

1
2
3
4
5
6
7
8
9
const promise = Promise.resolve(42);

promise
.finally(() => {
return 43; // 会被忽略
})
.then((value) => {
console.log(value); // 42
});

可以看到,最终传递给履行处理器的值是 42,而不是 43。因为解决处理器中返回语句被忽略了, 所以我们才可以用 then() 检索到上层 promise 传递来的原始值。这是调用 finally() 返回由复制上层 promise 状态和值所创建的 promise 的后果之一。

2.4 从链式 Promise 中返回 Promise

从 Promise 的处理器返回上层数据,使得我们能够在链中的多个 Promise 之间传递数据。 如果处理器返回的是一个对象,结果又会如何呢? 如果该对象是一个 Promise,那么我们需要一个额外的步骤来决定接下来该如何做。 考虑下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
const promise1 = Promise.resolve(42);
const promise2 = Promise.resolve(43);

promise1
.then((value) => {
console.log(value); // 42
return promise2; // 我们返回了一个 Promise
})
.then((value) => {
console.log(value); // 43
});

在这段代码中, promise1 被履行为 42, promise1 的履行处理器返回 promise2,这是一个已经处于履行状态的 promise, 第二个履行处理器之所以被调用是因为 promise2 处于履行状态, 如果 promise2 被拒绝,那么调用的就应该是拒绝处理器,而不是履行处理器。

关于这种模式,需要认识到的重要一点是, 第二个履行处理器并非被添加到 promise2 上, 而是被添加到第三个 Promise 上, 前面的代码等同于以下这段代码:

1
2
3
4
5
6
7
8
9
10
const promise1 = Promise.resolve(42);
const promise2 = Promise.resolve(43);
const promise3 = promise1.then((value) => {
console.log(value); // 42
return promise2;
});

promise3.then((value) => {
console.log(value); // 43
});

很明显,第二个履行处理器被添加到了 promise3 上, 而没有被添加到 promise2 上。 这是一个微妙但重要的细节, 因为如果 promise2 被拒绝, 那么第二个履行处理器将不会被调用。 来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const promise1 = Promise.resolve(42);
const promise2 = Promise.reject(43);

promise1
.then((value) => {
console.log(value); // 42
return promise2; // 我们返回了一个 Promise
})
.then((value) => {
console.log(value); // 永远不会被调用
})
.catch((reason) => {
console.error(reason); // 43
});

由于 promise2 被拒绝,因此这里调用了拒绝处理器。 promise2 的值 43 被传递给该拒绝处理器。

当一个操作需要不止一个 Promise 时, 从履行处理器返回 Promise 会很有用。 比如,fetch() 需要第二个 Promise 来读取响应主体。要读取一个 JSON 主体, 我们需要使用 response.json(), 它返回另一个 Promise。 以下是不使用链式 Promise 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const promise1 = fetch('book.json');

promise1
.then((response) => {
const promise2 = response.json();
promise2
.then((data) => {
console.log(data);
})
.catch((reason) => {
console.error(reason.message);
});
})
.catch((reason) => {
console.error(reason.message);
});

这段代码需要两个拒绝处理器来分别捕获两个步骤中可能出现的错误。 要简化代码,我们可以从第一个履行处理器返回第二个 Promise:

1
2
3
4
5
6
7
8
9
10
11
12
const promise1 = fetch('book.json');

promise1
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((reason) => {
console.error(reason.message);
});

在这里,当收到响应时,第一个履行处理器就会被调用,该履行处理器会返回另外一个 Promise,以 JSON 格式读取响应主体。当主体被读取并且有效数据准备就绪时,第二个履行处理器就会被调用。我们只需要在链式 Promise 的末端添加一个拒绝处理器,来捕获整个过程中可能发生的错误。

使用finally()从解决处理器返回 Promise 的效果与使用then()catch()不同。 除非finally()解决处理器抛出错误,或者返回一个处于拒绝状态的 Promise,否则其他任何的返回值(无论是返回一个普通的值还是返回一个 处于履行状态的 Promise)都会被忽略。 而原 Promise 的值将被采用, 如下所示:

1
2
3
4
5
6
7
8
9
const promise = Promise.resolve(42);

promise
.finally(() => {
return Promise.resolve(43); // 返回一个处于履行状态的 Promise, 会被忽略
})
.then((value) => {
console.log(value); // 42
});

在这个例子中,解决处理器返回了一个处于履行状态的 Promise, 值为 43, 但是下一个履行处理器接受到的值是原 promise 的值,即 42。

然而,如果我们从解决处理器返回一个处于拒绝状态的 promise, 那么该解决处理器返回的 promise 也会处于拒绝状态,如下所示:

1
2
3
4
5
6
7
8
9
const promise = Promise.resolve(42);

promise
.finally(() => {
return Promise.reject(43); // 返回一个处于拒绝状态的 Promise,会向下传递
})
.catch((reason) => {
console.log(reason); // 43
});

即使原 promise 是拒绝状态,这一点也成立,如下所示:

1
2
3
4
5
6
7
8
9
const promise = Promise.reject(42);

promise
.finally(() => {
return Promise.reject(43); // 返回一个处于拒绝状态的 Promise,会向下传递
})
.catch((reason) => {
console.log(reason); // 43
});

从解决处理器返回处于拒绝状态的 Promise 在功能上等同于抛出错误,返回的 Promise 因某个具体的理由而被拒绝。

从履行处理器或拒绝处理器返回 Promise 并不改变 Promise 执行器的执行时间。第一个 Promise 将首先运行其执行器,接着第二个 Promise 将运行其执行器,以此类推。通过返回 Promise,我们可以对 Promise 的结果定义额外的响应。要推迟履行处理器的执行时间,我们可以在一个履行处理器中创建新的 Promise,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const promise1 = Promise.resolve(42);

promise1
.then((value) => {
console.log(value); // 42
const promise2 = new Promise((resolve) => {
setTimeout(() => {
resolve(value + 1);
}, 1000);
});
return promise2;
})
.then((value) => {
console.log(value); // 43
});

我们在 promise1 的履行处理器中创建了一个新的 Promise。这意味着第二个履行处理器只有在 promise2 被履行后才执行。promise2 的执行器使用 setTimeout 在 1000 毫秒后解决这个 Promise。实际情况是,我们可能会发起网络请求或文件系统请求。如果我们想在开始一个新的异步操作之前,等待之前的 Promise 确定其状态,那么这种模式会很有用。

2.5 小结

  • 我们可以通过各种方式链接多个 Promise, 以在它们之间传递信息。 对then()catch()finally()的每次调用都会创建并返回一个新的 Promise, 只有当之前的 Promise 被解决时,该 Promise 才会被解决。如果 Promise 处理器返回一个普通的值, 那么这个值将成为这个处理器所创建的 Promise 的值(finally 注册的处理器返回一个普通的值会被忽略)。如果 Promise 处理器抛出一个错误, 那么这个错误会被捕获,并且返回的新的 Promise 将因这个错误而被拒绝。
  • 当一条链上的一个 Promise 被拒绝时, 链上的其他处理器所创建的 Promise 也都会被拒绝, 直到到达链的末端。鉴于此,我们建议在每条 Promise 链的末端都附加一个拒绝处理器, 以确保错误得到正确处理。没有捕获被拒绝的 Promise 将导致控制台输出错误消息, 或者抛出错误,一或者两者都有,取决于运行环境。
  • 我们可以从处理器返回 Promise,在这种情况下 对 Zen 和 Catch 的调用 所返回的 Promise 的状态和值取决于处理器中显式返回的 promise。(finally()处理器是个例外, 如果显式返回了一个处于履行状态的 promise 将会被忽略, 而显式返回处于拒绝状态的 promise 则会被继续往下传递)。我们可以利用这一点,将一些操作延迟到某个 Promise 履行之后再启动,并返回第二个 Promise 从而继续使用同一条 Promise 链。