Promise

Table of Contents

这篇 Promise 仅是摘录使用,内容很散碎……

使用 Promise1

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以我们首先说明怎样使用 Promise,再说明如何创建 Promise 。

本质上 Promise 是一个函数返回的对象 ,我们可以在它上面绑定回调函数,如此就不需要在一开始就把回调函数作为参数传入给这个函数了。

下面来看一个示例,假设现在有一个名为 createAudioFileAsync() 的函数,它接收一些配置和两个回调函数,然后异步地生成音频文件。一个回调函数在文件成功创建时被调用,另一个则在出现异常时被调用。

 1: // 成功时的回调函数
 2: function successCallback(result) {
 3:     console.log("音频文件创建成功:" + result);
 4: }
 5: 
 6: // 失败的回调函数
 7: function failureCallback(error) {
 8:     console.log("音频文件创建失败:" + error);
 9: }
10: 
11: createAudioFileAsync(audioSettings, successCallback, failureCallback);

更现代的函数会返回一个 Promise 对象,使得你可以将你的回调函数绑定在该 Promise 上。下面我们重写函数 createAudioFileAsync() 使其返回 Promise,如下:

1: const promise = createAudioFileAsync(audioSettings);
2: promise.then(successCallback, failureCallback);
3: 
4: // OR 简写为
5: createAudioFileAsync(audioSettings).then(successCallback, failureCallback);

我们把这个称为 异步函数调用 ,这种形式有若干优点,下面我们将会逐一讨论。

不同于“老式”的传入回调,在使用 Promise 时,会有 以下约定

  • 在本轮 事件循环 运行完成之前,回调函数是不会被调用的;
  • 即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用;
  • 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

Promise 很棒的一点就是 链式调用(chaining)

链式调用

连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链 来实现这种需求。

!!! then() 函数会返回一个和原来不同的新的 Promise 。

1: const promise = doSomething();
2: const promise2 = promise.then(successCallback, failureCallback);
3: 
4: // OR
5: const promise2 = doSomething().then(successCallback, failureCallback);

其中, promise2 不仅表示 doSomething() 函数的完成,也代表了你传入的 successCallback 或者 failureCallback 的完成,这两个函数也可以返回一个 Promise 对象,从而形成另一个异步操作,如此,在 promise2 上新增的回调函数会排上这个 Promise 对象的后面。

基本上,第一个 Promise 都代表了链中另一个异步过程的完成。

来看一下,过去要想做多重的异步操作,会导致经典的回调地狱,如下:

1: doSomething(function(result) {
2:     doSomethingElse(result, function(newResult) {
3:         doThirdThing(newResult, function(finalResult) {
4:             console.log('Got the final result: ' finalResult);
5:         }, failureCallback);
6:     }, failureCallback);
7: }, failureCallback);

而现在,我们可以把回调绑定到返回的 Promise 上,形成一个 Promise 链,如下:

 1: doSomething().then(Function(result) {
 2:     return doSomethingElse(result);
 3: })
 4:     .then(Function(newResult) {
 5:         return doThirdThing(newResult);
 6:     })
 7:     .then(function(finalResult) {
 8:         console.log('Got the final result: ' + finalResult);
 9:     })
10:     .catch(failureCallback);
11: 
12: // 也可以用箭头函数来表示
13: doSomething()
14:     .then(result => doSomethingElse(result))
15:     .then(newResult => doThirdThing(newResult))
16:     .then(finalResult => {
17:         console.log(`Got the final result: ${finalResult}`);
18:     })
19:     .catch(failureCallback);

!!!注意:一定要有返回值 ,否则,callback 将无法获取上一个 Promise 的结果。

then 里的参数是可选的, catch(failurecallback)then(null, failureCallback) 的缩略形式。

有可能会在一个回调失败之后继续使用链式操作,如下:

 1: new Promise((resolve, reject) => {
 2:     console.log('初始化');
 3:     resolve();
 4: })
 5:     .then(() => {
 6:         throw new Error('有哪里不对了');
 7:         console.log('执行「这个」”');
 8:     })
 9:     .catch(() => {
10:         console.log('执行「那个」');
11:     })
12:     .then(() => {
13:         console.log('执行「这个」,无论前面发生了什么');
14:     });
15: 
16: // 
17: // 初始化
18: // 执行“那个”
19: // 执行“这个”,无论前面发生了什么

错误传递

在之前 的回调地狱示例中,我们有 3 次 failureCallback 的调用,而在 Promise 链中只有尾部的一次调用。

通常,一遇到异常抛出,浏览器会顺着 Promise 链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。它和同步代码 try...catch... 的工作原理(执行过程)非常相似。

在 ES2017 标准的 async/await 语法糖中,这种异步代码的对称性得到了极致的体现,如下:

 1: async function foo() {
 2:     try {
 3:         const result = await doSomething();
 4:         const newResult = await doSomethingElse(result);
 5:         const finalResult = await doThirdThing(newResult);
 6:         console.log(`Got the final result: ${finalResult}`);
 7:     } catch(error) {
 8:         failureCallback(error);
 9:     }
10: }

通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。

当 Promise 被拒绝时,会有下文所述的两个事件( rejectionhandledunhandledrejection )之一被派发到全局作用域(通常而言,就是 window ;如果是在 web worker 中使用,就是 Worker 或者其他 worker-based 接口)。

此外暂时不深入,有兴趣的时候再了解。

在旧式回调 API 中创建 Promise

可以通过 Promise 的构造器从零开始创建 Promise 。这种方式(通过构造器的方式)应当只在封装旧 API 的时候用到。

理想状态下,所有的异步函数都已经返回 Promise 了,但有一些 API 仍然使用旧方式传入的成功(或失败)的回调。典型的例子就是 setTimeout() 函数。

1: setTimeout(() => saySomething("10 seconds passed"), 10000);

混用旧式回调和 Promise 可能会造成运行时序的问题,如果 saySomething 函数失败了,或者包含了编程错误,就没有办法捕获它了。

幸运地是,我们可以用 Promise 来封闭它。最好的做法是,将这些有问题的函数封闭起来,留在底层,并且永远不要再直接调用它们。

1: const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
2: 
3: wait(10000).then(() => saySomething("10 seconds passed")).catch(failureCallback);

通常,Promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise 。既然 setTimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject 。

Promise.resolve()Promise.reject() 是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法,它们有时很有用。

关于 Promise.resolve()

Promise.resolve(value) 方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 promise,那么将返回这个 promise ;如果这个值是 thenable (即带有 then 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终形态;否则返回的 promise 将以此值完成。此函数将类 promise 对象的多层嵌套展平。

注意,不要在解析为自身的 thenable 上调用 Promise.resolve ,这将导致无限递归,因为它试图展平无限嵌套的 promise 。如下:

1: let thenable = {
2:     then: (resolve, reject) => {
3:         resolve(thenable)
4:     }
5: }
6: 
7: Promise.resolve(thenable);      // 这会造成一个死循环

我们看一些使用静态 Promise.resolve 方法的示例:

 1: // 1. resolve 一个字符串
 2: Promise.resolve("Success").then(function(value) {
 3:     console.log(value); // →  "Success"
 4: }, function(value) {
 5:     // 不会被调用
 6: });
 7: 
 8: // 2. resolve 一个数组
 9: var p = Promise.resolve([1, 2, 3]);
10: p.then(function(v) {
11:     console.log(v[0]);          // →  1
12: })
13: 
14: // 3. resolve 另一个 promise
15: var original = Promise.resolve(33);
16: var cast = Promise.resolve(original);
17: cast.then(function(value) {
18:     console.log('value: ' + value);
19: });
20: console.log('original === cast ? ' + (original === cast));
21: // → value: 33
22: // → original === cast ? true
23: 
24: // 4. resolve thenable 并抛出错误
25: var p1 = Promise.resolve({
26:     then: function(onFulfill, onReject) { onFulfill("fulfilled!"); }
27: });
28: console.log(p1 instanceof Promise); // → true,这是一个 Promise 对象
29: 
30: p1.then(function(v) {
31:     console.log(v);             // → "fulfilled!"
32: }, function(e) {
33:     // 不会被调用
34: });
35: 
36: // Thenable 在 callback 之前抛出异常
37: // Promise rejects
38: var thenable = { then: function(resolve) {
39:     throw new TypeError("Throwing");
40:     resolve("Resolving");
41: }};
42: 
43: var p2 = Promise.resolve(thenable);
44: p2.then(function(v) {
45:     // 不会被调用
46: }, Function(e) {
47:     console.log(e);             // TypeError: Throwing
48: });
49: 
50: // Thenable在callback之后抛出异常
51: // Promise resolves
52: var thenable = { then: function(resolve) {
53:     resolve("Resolving");
54:     throw new TypeError("Throwing");
55: }};
56: 
57: var p3 = Promise.resolve(thenable);
58: p3.then(function(v) {
59:     console.log(v); // 输出"Resolving"
60: }, function(e) {
61:     // 不会被调用
62: });
63: 

时序

为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 函数也总是会被异步调用:

1: Promise.resolve().then(() => console.log(2));
2: console.log(1);
3: // → 1
4: // → 2

传递到 then() 中的函数被置入到一个微任务队列中,而不是立即执行,这意味着它是在 JavaScript 事件队列的所有运行时结束了,且事件队列被清空之后,才开始执行:

1: const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
2: wait().then(() => console.log(4));
3: 
4: Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
5: 
6: console.log(1);
7: // → 1, 2, 3, 4
仔细观察你会发现,setTimeout 和 then 的执行时机是有区别的,什么区别呢?不妨去探索一下。

除了上述提到的这些,Promise 还有 Promise.all() 、Promise.race() 等方法,用时再查即可。我们已经对 Promise 的使用有了初步的了解,那么 Promise 到底是什么呢?

Promise 是什么

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。

一个 Promise 对象代表一个在这个 Promise 被创建出来时不一定已知的值 。异步方法并不会立即返回最终的值,而是会返回一个 promise ,以便在未来某个时候把值交给使用者。

可以把 Promise 理解为一个承载异步请求响应状态的容器。

异步操作都有那些状态呢?

  • pending 待定 - 初始状态,既没有兑现,也没有拒绝;
  • fulfilled 已兑现 - 意味着操作成功完成;
  • rejected 已拒绝 - 意味着操作失败。

这里需要注意的地方在于, new Promise(executorFunc) 中的参数函数 excutorFunc 只会在 Promise 创建的时候执行一次,并固定执行后的结果不再改变。

其实,这里很容易理解,一个操作要么是在运行中,还没出结果;要么就是成功了,或者失败了。

我们可以用 promise.then()、 promise.catch()promise.finally() 这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。这些方法还会返回一个新生成的 promise 对象,这个对象可以被非强制性的用来做链式调用。

不要和惰性求值混淆: 有一些语言中有惰性求值和延时计算的特性,它们也被称为“promises”,例如 Scheme。JavaScript 中的 promise 代表的是已经正在发生的进程, 而且可以通过回调函数实现链式调用。 如果您想对一个表达式进行惰性求值,就考虑一下使用无参数的"箭头函数": f = () =>表达式 来创建惰性求值的表达式,使用 f() 求值。

构造函数 Promise()

创建一个新的 Promise 对象。该构造函数主要用于包装还没有添加 promise 支持的函数。

Promise 原型

Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数—— resolvereject ——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用 reject 函数。

1: const myFirstPromise = new Promise((resolve, reject) => {
2:     // ?做一些异步操作,最终会调用两者之一:
3:     //     resolve(someValue);       // fulfilled
4:     // ?OR
5:     //     reject("failure reason"); // rejected
6: })

想要某个函数拥有 promise 功能,只需让其返回一个 promise 即可。

 1: // 示例 1
 2: function myAsyncFunction(url) {
 3:     return new Promise((resolve, reject) => {
 4:         const xhr = new XMLHttpRequest();
 5:         xhr.open("GET", url);
 6:         xhr.onload = () => resolve(xhr.responseText);
 7:         xhr.onerror = () => reject(xhr.statusText);
 8:         xhr.send();
 9:     });
10: }
11: 
12: // 示例 2
13: let myFirstPromise = new Promise(function(resolve, reject){
14:     // 当异步代码执行成功时,我们才会调用 resolve(...), 当异步代码失败时就会调用 reject(...)
15:     // 在本例中,我们使用 setTimeout(...) 来模拟异步代码,实际编码时可能是 XHR 请求或是 HTML5 的一些 API 方法.
16:     setTimeout(function(){
17:         resolve("成功!"); // 代码正常执行!
18:     }, 250);
19: });
20: 
21: myFirstPromise.then(function(successMessage){
22:     // successMessage 的值是上面调用 resolve(...) 方法传入的值.
23:     // successMessage 参数不一定非要是字符串类型,这里只是举个例子
24:     console.log("Yay! " + successMessage);
25: });

众所周知,JavaScript 也是一门面向对象的编程语言,只不过它是基于原型的。Promise() 本身是一个构造函数(可以作个不恰当的类比 - Promise 是一个类),其上包含一些静态方法(即类本身的静态方法,与实例无关),如: Promise.all(iterable)、 Promise.allSettled(iterable)、 Promise.any(iterable)、 Promise.race(iterable)、 Promise.reject(reason)Promise.resolve(value) 等。

另外,如 then()、 catch()finally() 等方法则是定义在 Promise.prototype 原型上的。

TODO 延伸阅读

Footnotes:

Date: 2022-02-08 Tue 14:34

Author: Jack Liu

Created: 2022-05-05 Thu 20:16

豫ICP备19900901号