ES6读书笔记-Promise与异步编程的爱恨纠葛
异步编程的背景
JS 引擎建立在单线程事件循环的概念上。 单线程( Single - threaded) 意味着同一时刻只能
执行一段代码, 与 Java 或 C++这种允许同时执行多段不同代码的多线程语言形成了反差。
多段代码可以同时访问或修改状态, 维护并保护这些状态就变成了难题, 这也是基于多线程
的软件中出现 bug 的常见根源之一。
JS 引擎在同一时刻只能执行一段代码, 所以引擎无须留意那些“ 可能” 运行的代码。 代码会被
放置在作业队列( job queue) 中, 每当一段代码准备被执行, 它就会被添加到作业队列。
当 JS 引擎结束当前代码的执行后, 事件循环就会执行队列中的下一个作业。 事件循环(
event loop) 是 JS 引擎的一个内部处理线程, 能监视代码的执行并管理作业队列。 要记住
既然是一个队列, 作业就会从队列中的第一个开始, 依次运行到最后一个。
事件模型
当用户点击一个按钮或按下键盘上的一个键时, 一个事件( event)—— 例如 onclick——
就被触发了。 该事件可能会对此交互进行响应, 从而将一个新的作业添加到作业队列的尾
部。 这就是 JS 关于异步编程的最基本形式。 事件处理程序代码直到事件发生后才会被执行,
此时它会拥有合适的上下文。 例如:
let button = document.getElementById("my-btn");
button.onclick = function (event) {
console.log("Clicked");
};
在此代码中,console.log("Clicked") 直到 button 被点击后才会被执行。 当 button 被点
击, 赋值给 onclick 的函数就被添加到作业队列的尾部,并在队列前部所有任务结束之后再
执行。
事件可以很好地工作于简单的交互, 但将多个分离的异步调用串联在一起却会很麻烦, 因为
必须追踪每个事件的事件对象( 例如上例中的 button)。 此外, 你还需确保所有的事件处
理程序都能在事件第一次触发之前被绑定完毕。 例如, 若 button 在 onclick 被绑定之前就
被点击, 那就不会有任何事发生。 因此虽然在响应用户交互或类似的低频功能时, 事件很有
用, 但它在面对更复杂的需求时仍然不够灵活。
回调模式
当 Node.js 被创建时,它通过普及回调函数编程模式提升了异步编程模型。回调函数模式类
似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函
数(即回调函数)是作为参数传入的,如下所示:
readFile("example.txt", function (err, contents) {
if (err) {
throw err;
}
console.log(contents);
});
console.log("Hi!");
此例使用了 Node.js 惯例, 即错误优先( error - first) 的回调函数风格。 readFile() 函数
用于读取磁盘中的文件( 由第一个参数指定), 并在读取完毕后执行回调函数( 即第二个参
数)。 如果存在错误, 回调函数的 err 参数会是一个错误对象; 否则 contents 参数就会以
字符串形式包含文件内容。
使用回调函数模式, readFile() 会立即开始执行, 并在开始读取磁盘时暂停。 这意味着
console.log("Hi!") 会在 readFile() 被调用后立即进行输出, 要早于
console.log(contents) 的打印操作。 当 readFile() 结束操作后, 它会将回调函数以及相关
参数作为一个新的作业添加到作业队列的尾部。 在之前的作业全部结束后, 该作业才会执
行。
回调函数模式要比事件模型灵活得多, 因为使用回调函数串联多个调用会相对容易。 例如:
readFile("example.txt", function (err, contents) {
if (err) {
throw err;
}
writeFile("example.txt", function (err) {
if (err) {
throw err;
}
console.log("File was written!");
});
});
在此代码中, 对于 readFile() 的一次成功调用引出了另一个异步调用, 即调用
writeFile() 函数。 注意这两个函数都使用了检查 err 的同一基本模式。 当 readFile() 执
行结束后, 它添加一个作业到作业队列, 从而导致 writeFile() 在之后被调用( 假设没有出
现错误)。 接下来, writeFile() 也会在执行结束后向队列添加一个作业。
这种模式运作得相当好, 但你可能会迅速察觉陷入了回调地狱( callback hell), 这会在嵌
套过多回调函数时发生, 就像这样:
method1(function (err, result) {
if (err) {
throw err;
}
method2(function (err, result) {
if (err) {
throw err;
}
method3(function (err, result) {
if (err) {
throw err;
}
method4(function (err, result) {
if (err) {
throw err;
}
method5(result);
});
});
});
});
像本例一样嵌套多个方法调用会创建错综复杂的代码, 会难以理解与调试。 当想要实现更复
杂的功能时, 回调函数也会存在问题。 要是你想让两个异步操作并行运行, 并且在它们都结
束后提醒你, 那该怎么做? 要是你想同时启动两个异步操作, 但只采用首个结束的结果, 那又该怎么做?
在这些情况下, 你需要追踪多个回调函数并做清理操作, Promise 能大幅度改善这种情况。
Promise 基础
Promise 是为异步操作的结果所准备的占位符。 函数可以返回一个 Promise, 而不必订阅一个
事件或向函数传递一个回调参数, 就像这样:
// readFile 承诺会在将来某个时间点完成
let promise = readFile("example.txt");
在此代码中, readFile() 实际上并未立即开始读取文件,这将会在稍后发生。此函数反而
会返回一个 Promise 对象以表示异步读取操作,因此你可以在将来再操作它。你能对结果进
行操作的确切时刻,完全取决于 Promise 的生命周期是如何进行的。
Promise 的生命周期
每个 Promise 都会经历一个短暂的生命周期, 初始为挂起态( pending state), 这表示异步
操作尚未结束。 一个挂起的 Promise 也被认为是未决的( unsettled)。 上个例子中的
Promise 在 readFile() 函数返回它的时候就是处在挂起态。 一旦异步操作结束, Promise
就会被认为是已决的( settled), 并进入两种可能状态之一:
- 已完成( fulfilled): Promise 的异步操作已成功结束;
- 已拒绝( rejected): Promise 的异步操作未成功结束, 可能是一个错误, 或由其他原
因导致。
内部的[[PromiseState]] 属性会被设置为 "pending"、"fulfilled"或 "rejected",
以反映 Promise 的状态。 该属性并未在 Promise 对象上被暴露出来, 因此你无法以编程方式判
断 Promise 到底处于哪种状态。 不过你可以使用 then() 方法在 Promise 的状态改变时执行
一些特定操作。
译注: 相关词汇翻译汇总
- pending: 挂起, 表示未结束的 Promise 状态。 相关词汇“ 挂起态”。
- fulfilled: 已完成, 表示已成功结束的 Promise 状态, 可以理解为“ 成功完成”。 相关
词汇“ 完成”、“ 被完成”、“ 完成态”。- rejected: 已拒绝, 表示已结束但失败的 Promise 状态。 相关词汇“ 拒绝”、“ 被拒
绝”、“ 拒绝态”。- resolve: 决议, 表示将 Promise 推向成功态, 可以理解为“ 决议通过”, 在 Promise
概念中与“ 完成” 是近义词。 相关词汇“ 决议态”、“ 已决议”、“ 被决议”。- unsettled: 未决, 或者称为“ 未解决”, 表示 Promise 尚未被完成或拒绝, 与“ 挂
起” 是近义词。- settled: 已决, 或者称为“ 已解决”, 表示 Promise 已被完成或拒绝。 注意这与“ 已完
成” 或“ 已决议” 不同,“ 已决” 的状态也可能是“ 拒绝态”( 已失败)。- fulfillment handler: 完成处理函数, 表示 Promise 为完成态时会被调用的函数。
- rejection handler: 拒绝处理函数, 表示 Promise 为拒绝态时会被调用的函数。
then() 方法在所有的 Promise 上都存在, 并且接受两个参数。 第一个参数是 Promise 被完
成时要调用的函数, 与异步操作关联的任何附加数据都会被传入这个完成函数。 第二个参数
则是 Promise 被拒绝时要调用的函数, 与完成函数相似, 拒绝函数会被传入与拒绝相关联的
任何附加数据。
用这种方式实现 then() 方法的任何对象都被称为一个 thenable。 所有的 Promise 都是
thenable, 反之则未必成立。
传递给 then() 的两个参数都是可选的, 因此你可以监听完成与拒绝的任意组合形式。 例
如, 研究这组 then() 调用:
let promise = readFile("example.txt");
promise.then(function (contents) {
// 完成
console.log(contents);
}, function (err) {
// 拒绝
console.error(err.message);
});
promise.then(function (contents) {
// 完成
console.log(contents);
});
promise.then(null, function (err) {
// 拒绝
console.error(err.message);
});
这三个 then() 调用都操作在同一个 Promise 上。 第一个调用同时监听了完成与失败; 第二
个调用只监听了完成, 错误不会被报告; 第三个则只监听了拒绝, 并不报告成功信息。
Promis 也具有一个
catch () 方法, 其行为等同于只传递拒绝处理函数给 then()。 例如,
以下的
catch () 与 then() 调用是功能等效的。
promise.catch(function (err) {
// 拒绝
console.error(err.message);
});
// 等同于:
promise.then(null, function (err) {
// 拒绝
console.error(err.message);
});
then() 与 catch () 背后的意图是让你组合使用它们来正确处理异步操作的结果。 此系统要
优于事件与回调函数, 因为它让操作是成功还是失败变得完全清晰( 事件模式倾向于在出错
时不被触发, 而在回调函数模式中你必须始终记得检查错误参数)。 只需知道若你未给
Promise 附加拒绝处理函数, 所有的错误就会静默发生。 建议始终附加一个拒绝处理函数,
即使该处理程序只是用于打印错误日志。
即使完成或拒绝处理函数在 Promise 已经被解决之后才添加到作业队列, 它们仍然会被执
行。 这允许你随时添加新的完成或拒绝处理函数, 并保证它们会被调用。 例如:
let promise = readFile("example.txt");
// 原始的完成处理函数
promise.then(function (contents) {
console.log(contents);
// 现在添加另一个
promise.then(function (contents) {
console.log(contents);
});
});
在此代码中, 完成处理函数又为同一个 Promise 添加了另一个完成处理函数。 这个 Promise
此刻已经完成了, 因此新的处理程序就被添加到任务队列, 并在就绪时( 前面的作业执行完
毕后) 被调用。 拒绝处理函数使用同样方式工作。
每次调用 then() 或
catch () 都会创建一个新的作业, 它会在 Promise 已决议时被执
行。 但这些作业最终会进入一个完全为 Promise 保留的作业队列。 这个独立队列的确切
细节对于理解如何使用 Promise 是不重要的, 你只需理解作业队列通常来说是如何工作
的。
创建未决的 Promise
新的 Promise 使用 Promise 构造器来创建。 此构造器接受单个参数: 一个被称为执行器(
executor) 的函数, 包含初始化 Promise 的代码。 该执行器会被传递两个名为 resolve()
与 reject() 的函数作为参数。 resolve() 函数在执行器成功结束时被调用, 用于示意该
Promise 已经准备好被决议( resolved), 而 reject() 函数则表明执行器的操作已失败。
此处有个范例, 在 Node.js 中使用了一个 Promise, 实现了本章前面的 readFile() 函数:
// Node.js 范例
let fs = require("fs");
function readFile(filename) {
return new Promise(function (resolve, reject) {
// 触发异步操作
fs.readFile(filename, {
encoding: "utf8"
}, function (err, contents) {
// 检查错误
if (err) {
reject(err);
return;
}
// 读取成功
resolve(contents);
});
});
}
let promise = readFile("example.txt");
// 同时监听完成与拒绝
promise.then(function (contents) {
// 完成
console.log(contents);
}, function (err) {
// 拒绝
console.error(err.message);
});
在此例中, Node.js 原生的 fs.readFile() 异步调用被包装在一个 Promise 中。 执行器要么
传递错误对象给 reject() 函数, 要么传递文件内容给 resolve() 函数。
要记住执行器会在 readFile() 被调用时立即运行。 当 resolve() 或 reject() 在执行器内
部被调用时, 一个作业被添加到作业队列中, 以便决议( resolve) 这个 Promise。 这被称
为作业调度( job scheduling), 若你曾用过 setTimeout() 或 setInterval() 函数, 那么
应该已经熟悉这种方式。 在作业调度中, 你添加新作业到队列中是表示:“ 不要立刻执行这个
作业, 但要在稍后执行它”。 例如, setTimeout() 函数能让你指定一个延迟时间, 延迟之后
作业才会被添加到队列:
// 在 500 毫秒之后添加此函数到作业队列
setTimeout(function () {
console.log("Timeout");
}, 500);
console.log("Hi!");
此代码安排一个作业在 500 毫秒之后被添加到作业队列。 此处两个 console.log() 调用产生
了以下输出:
Hi!
Timeout
译注: 范例中的输出顺序与 500 毫秒的延时没有关系, 而与 setTimeout()
的机制有关。 我们可以把延时改为 0, 依然可以得到相同的结果:
// 在 0 毫秒之后添加此函数到作业队列
setTimeout(function () {
console.log("Timeout");
}, 0);
console.log("Hi!");
Promise 工作方式与之相似。 Promise 的执行器会立即执行, 早于源代码中在其之后的任何
代码。 例如:
let promise = new Promise(function (resolve, reject) {
console.log("Promise");
resolve();
});
console.log("Hi!");
此代码的输出结果为:
Promise
Hi!
调用 resolve() 触发了一个异步操作。 传递给 then() 与
catch () 的函数会异步地被执
行, 并且它们也被添加到了作业队列( 先进队列再执行)。 此处有个例子:
let promise = new Promise(function (resolve, reject) {
console.log("Promise");
resolve();
});
promise.then(function () {
console.log("Resolved.");
});
console.log("Hi!");
此例的输出结果为:
Promise
Hi!
Resolved
创建已决的 Promise
基于 Promise 执行器行为的动态本质, Promise 构造器就是创建未决的 Promise 的最好方
式。 但若你想让一个 Promise 代表一个已知的值, 那么安排一个单纯传值给 resolve() 函数
的作业并没有意义。 相反, 有两种方法可使用指定值来创建已决的 Promise。
使用 Promise.resolve()
Promise.resolve() 方法接受单个参数并会返回一个处于完成态的 Promise。 这意味着没有
任何作业调度会发生, 并且你需要向 Promise 添加一个或更多的完成处理函数来提取这个参
数值。 例如:
let promise = Promise.resolve(42);
promise.then(function (value) {
console.log(value); // 42
});
此代码创建了一个已完成的 Promise ,因此完成处理函数就接收到 42 作为 value 参数。若一个拒绝处理函数被添加到此 Promise ,该拒绝处理函数将永不会被调用,因为此 Promise
绝不可能再是拒绝态。
使用Promise.reject()
你也可以使用 Promise.reject() 方法来创建一个已拒绝的 Promise。 此方法像
Promise.resolve() 一样工作, 区别是被创建的 Promise 处于拒绝态, 如下:
let promise = Promise.reject(42);
promise.catch(function (value) {
console.log(value); // 42
});
非 Promise 的 Thenable
Promise.resolve() 与 Promise.reject() 都能接受非 Promise 的 thenable 作为参数。 当传
入了非 Promise 的 thenable 时, 这些方法会创建一个新的 Promise, 此 Promise 会在
then() 函数之后被调用。
当一个对象拥有一个能接受 resolve 与 reject 参数的 then() 方法, 该对象就会被认为是
一个非 Promise 的 thenable, 就像这样:
let thenable = {
then: function (resolve, reject) {
resolve(42);
}
};
此例中的 thenable 对象, 除了 then() 方法之外没有任何与 Promise 相关的特征。 你可以
调用 Promise.resolve() 来将 thenable 转换为一个已完成的 Promise:
let thenable = {
then: function (resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
在此例中, Promise.resolve() 调用了 thenable.then(), 确定了这个 thenable 的
Promise 状态: 由于 resolve(42) 在 thenable.then() 方法内部被调用, 这个 thenable 的
Promise 状态也就被设为已完成。 一个名为 p1 的新 Promise 被创建为完成态, 并从
thenable 中接收到了值( 此处为 42), 于是 p1 的完成处理函数就接收到一个值为 42 的
参数。
使用 Promise.resolve(), 同样还能从一个 thenable 创建一个已拒绝的 Promise:
let thenable = {
then: function (resolve, reject) {
reject(42);
}
};
let p1 = Promise.resolve(thenable);
p1.catch(function (value) {
console.log(value); // 42
});
此例类似于上例, 区别是此处的 thenable 被拒绝了。 当 thenable.then() 执行时, 一个处
于拒绝态的新 Promise 被创建, 并伴随着一个值( 42)。 这个值此后会被传递给 p1 的拒
绝处理函数。
Promise.resolve() 与 Promise.reject() 用类似方式工作, 让你能轻易处理非 Promise 的
thenable。 在 Promise 被引入 ES6 之前, 许多库都使用了 thenable, 因此将 thenable 转换
为正规 Promise 的能力就非常重要了, 能对之前已存在的库提供向下兼容。 当你不能确定一
个对象是否是 Promise 时, 将该对象传递给 Promise.resolve() 或 Promise.reject()( 取
决于你的预期结果) 是能找出的最好方式, 因为传入真正的 Promise 只会被直接传递出来,
并不会被修改。
执行器错误
如果在执行器内部抛出了错误, 那么 Promise 的拒绝处理函数就会被调用。 例如:
let promise = new Promise(function (resolve, reject) {
throw new Error("Explosion!");
});
promise.catch(function (error) {
console.log(error.message); // "Explosion!"
});
在此代码中, 执行器故意抛出了一个错误。 此处在每个执行器之内并没有显式的
try -
catch, 因此错误就被捕捉并传递给了拒绝处理函数。 这个例子等价于:
let promise = new Promise(function (resolve, reject) {
try {
throw new Error("Explosion!");
} catch (ex) {
reject(ex);
}
});
promise.catch(function (error) {
console.log(error.message); // "Explosion!"
});
执行器处理程序捕捉了抛出的任何错误, 以简化这种常见处理。 但在执行器内抛出的错误仅
当存在拒绝处理函数时才会被报告, 否则这个错误就会被隐瞒。 这在开发者早期使用
Promise 的时候是一个问题, 但 JS 环境通过提供钩子( hook) 来捕捉被拒绝的 Promise,
从而解决了此问题。
串联 Promise
到此为止, Promise 貌似不过是个对组合使用回调函数与 setTimeout() 函数的增量改进,
然而 Promise 的内容远比表面上所看到的更多。 更确切地说, 存在多种方式来将 Promise 串
联在一起, 以完成更复杂的异步行为。
每次对 then() 或
catch () 的调用实际上创建并返回了另一个 Promise, 仅当前一个
Promise 被完成或拒绝时, 后一个 Promise 才会被决议。 研究以下例子:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
p1.then(function (value) {
console.log(value);
}).then(function () {
console.log("Finished");
});
此代码输出:
42
Finished
对 p1.then() 的调用返回了第二个 Promise, 又在这之上调用了 then()。 仅当第一个
Promise 已被决议后, 第二个 then() 的完成处理函数才会被调用。 假若你在此例中不使用
串联, 它看起来就会是这样:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = p1.then(function (value) {
console.log(value);
})
p2.then(function () {
console.log("Finished");
});
在这个无串联版本的代码中, p1.then() 的结果被存储在 p2 中, 并且随后 p2.then() 被
调用, 以添加最终的完成处理函数。 正如你可能已经猜到的, 对于 p2.then() 的调用也返回
了一个 Promise, 本例只是未使用此 Promise。
捕获错误
Promise 链允许你捕获前一个 Promise 的完成或拒绝处理函数中发生的错误。例如:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
p1.then(function (value) {
throw new Error("Boom!");
}).catch(function (error) {
console.log(error.message); // "Boom!"
});
在此代码中, p1 的完成处理函数抛出了一个错误, 链式调用指向了第二个 Promise 上的
catch () 方法, 能通过此拒绝处理函数接收前面的错误。 若是一个拒绝处理函数抛出了错
误, 情况也是一样:
let p1 = new Promise(function (resolve, reject) {
throw new Error("Explosion!");
});
p1.catch(function (error) {
console.log(error.message); // "Explosion!"
throw new Error("Boom!");
}).catch(function (error) {
console.log(error.message); // "Boom!"
});
此处的执行器抛出了一个错误, 就触发了 p1 这个 Promise 的拒绝处理函数, 该处理函数随
后抛出了另一个错误, 并被第二个 Promise 的拒绝处理函数所捕获。 链式 Promise 调用能察
觉到链中其他 Promise 中的错误。
为了确保能正确处理任意可能发生的错误, 应当始终在 Promise 链尾部添加拒绝处理函数。
在 Promise 链中返回值
Promise 链的另一重要方面是能从一个 Promise 传递数据给下一个 Promise 的能力。 传递给
执行器中的 resolve() 处理函数的参数, 会被传递给对应 Promise 的完成处理函数, 这点你
前面已看到过了。 你可以指定完成处理函数的返回值, 以便沿着一个链继续传递数据。 例
如:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
p1.then(function (value) {
console.log(value); // "42"
return value + 1;
}).then(function (value) {
console.log(value); // "43"
});
p1 的完成处理函数在被执行时返回了 value + 1。 由于 value 的值为 42( 来自执行
器), 此完成处理函数就返回了 43。 这个值随后被传递给第二个 Promise 的完成处理函数,
并被其输出到控制台。
你能对拒绝处理函数做相同的事。 当一个拒绝处理函数被调用时, 它也能返回一个值。 如果
这么做, 该值会被用于完成下一个 Promise, 就像这样:
let p1 = new Promise(function (resolve, reject) {
reject(42);
});
p1.catch(function (value) {
// 第一个完成处理函数
console.log(value); // "42"
return value + 1;
}).then(function (value) {
// 第二个完成处理函数
console.log(value); // "43"
});
此处的执行器使用 42 调用了 reject(), 该值被传递到这个 Promise 的拒绝处理函数中, 从
中又返回了 value + 1。 尽管后一个返回值是来自拒绝处理函数, 它仍然被用于链中下一个
Promise 的完成处理函数。 若有必要, 一个 Promise 的失败可以通过传递返回值来恢复整个
Promise 链。
在 Promise 链中返回 Promise
从完成或拒绝处理函数中返回一个基本类型值, 能够在 Promise 之间传递数据, 但若你返回
的是一个对象呢? 若该对象是一个 Promise, 那么需要采取一个额外步骤来决定如何处理。
研究以下例子:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
resolve(43);
});
p1.then(function (value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
}).then(function (value) {
// 第二个完成处理函数
console.log(value); // 43
});
在此代码中, p1 安排了一个决议 42 的作业, p1 的完成处理函数返回了一个已处于决议
态的 Promise: p2。 由于 p2 已被完成, 第二个完成处理函数就被调用了。 而若 p2 被
拒绝, 会调用拒绝处理函数( 如果存在的话), 而不调用第二个完成处理函数。
关于此模式需认识的首要重点是第二个完成处理函数并未被添加到 p2 上, 而是被添加到第
三个 Promise。 正因为此, 上个例子就等价于:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
resolve(43);
});
let p3 = p1.then(function (value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
});
p3.then(function (value) {
// 第二个完成处理函数
console.log(value); // 43
});
此处清楚说明了第二个完成处理函数被附加给 p3 而不是 p2。 这是一个细微但重要的区
别, 因为若 p2 被拒绝, 则第二个完成处理函数就不会被调用。 例如:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
reject(43);
});
p1.then(function (value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
}).then(function (value) {
// 第二个完成处理函数
console.log(value); // 永不被调用
});
在此例中, 由于 p2 被拒绝了, 第二个完成处理函数就永不被调用。 不过你可以改为对其附
加一个拒绝处理函数:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
reject(43);
});
p1.then(function (value) {
// 第一个完成处理函数
console.log(value); // 42
return p2;
}).catch(function (value) {
// 拒绝处理函数
console.log(value); // 43
});
此处 p2 被拒绝, 导致拒绝处理函数被调用, 来自 p2 的拒绝值 43 会被传递给拒绝处理函
数。
从完成或拒绝处理函数中返回 thenable, 不会对 Promise 执行器何时被执行有所改变。 第一
个被定义的 Promise 将会首先运行它的执行器, 接下来才轮到第二个 Promise 的执行器执
行, 以此类推。 返回 thenable 只是让你能在 Promise 结果之外定义附加响应。 你能通过在完
成处理函数中创建一个新的 Promise, 来推迟完成处理函数的执行。 例如:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
p1.then(function (value) {
console.log(value); // 42
// 创建一个新的 promise
let p2 = new Promise(function (resolve, reject) {
resolve(43);
});
return p2
}).then(function (value) {
console.log(value); // 43
});
在此例中, 一个新的 Promise 在 p1 的完成处理函数中被创建。 这意味着直到 p2 被完成之
后, 第二个完成处理函数才会执行。 若你想等待前面的 Promise 被解决, 之后才去触发另一
个 Promise, 那么这种模式就非常有用。
响应多个 Promise
ES6 提供了能监视多个 Promise 的两个方法:
Promise.all() 与 Promise.race() 。
Promise.all() 方法
Promise.all() 方法接收单个可迭代对象( 如数组) 作为参数, 并返回一个 Promise。 这个
可迭代对象的元素都是 Promise, 只有在它们都完成后, 所返回的 Promise 才会被完成。 例
如:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
resolve(43);
});
let p3 = new Promise(function (resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function (value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
此处前面的每个 Promise 都用一个数值进行了决议, 对 Promise.all() 的调用创建了新的
Promise p4, 在 p1、 p2 与 p3 都被完成后, p4 最终会也被完成。 传递给 p4 的完
成处理函数的结果是一个包含每个决议值( 42、 43 与 44) 的数组, 这些值的存储顺序保
持了待决议的 Promise 的顺序( 与完成的先后顺序无关), 因此你可以将结果匹配到每个
Promise。
若传递给 Promise.all() 的任意 Promise 被拒绝了, 那么方法所返回的 Promise 就会立刻被
拒绝, 而不必等待其他的 Promise 结束:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
});
let p2 = new Promise(function (resolve, reject) {
reject(43);
});
let p3 = new Promise(function (resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function (value) {
console.log(Array.isArray(value)) // false
console.log(value); // 43
});
在此例中, p2 被使用数值 43 进行了拒绝, 则 p4 的拒绝处理函数就立刻被调用, 而不会
等待 p1 或 p3 结束执行( 它们仍然会各自结束执行, 只是 p4 不等它们)。
拒绝处理函数总会接收到单个值, 而不是一个数组, 该值就是被拒绝的 Promise 所返回的拒
绝值。 本例中的拒绝处理函数被传入了 43, 反映了来自 p2 的拒绝。
Promise.race() 方法
Promise.race() 提供了监视多个 Promise 的一个稍微不同的方法。 此方法也接受一个包含需
监视的 Promise 的可迭代对象, 并返回一个新的 Promise, 但一旦来源 Promise 中有一个被
解决, 所返回的 Promise 就会立刻被解决。 与等待所有 Promise 完成的 Promise.all() 方法
不同, 在来源 Promise 中任意一个被完成时, Promise.race() 方法所返回的 Promise 就能
作出响应。 例如:
let p1 = Promise.resolve(42);
let p2 = new Promise(function (resolve, reject) {
resolve(43);
});
let p3 = new Promise(function (resolve, reject) {
resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function (value) {
console.log(value); // 42
});
在此代码中, p1 被创建为一个已完成的 Promise, 而其他的 Promise 则需要调度作业。
p4 的完成处理函数被使用数值 42 进行了调用, 并忽略了其他的 Promise。 传递给
Promise.race() 的 Promise 确实在进行赛跑, 看哪一个首先被解决。 若胜出的 Promise 是被
完成, 则返回的新 Promise 也会被完成; 而胜出的 Promise 若是被拒绝, 则新 Promise 也会
被拒绝。
总结
-
Promise 被设计用于改善 JS 中的异步编程, 与事件及回调函数对比, 在异步操作方面为你提 供了更多的控制权与组合性。
-
Promise 调度被添加到 JS 引擎作业队列, 以便稍后执行。 不 过此处有另一个作业队列追踪Promise的完成与拒绝处理函数,
以确保适当的执行。 -
Promise 具有三种状态: 挂起、 已完成、 已拒绝。 一个 Promise 起始于挂起态, 并在成功时 转为完成态,或在失败时转为拒绝态。 在这两种情况下, 处理函数都能被添加以表明 Promise 何时被解决。 then()方法允许你绑定完成处理函数与拒绝处理函数, 而 catch ()方法则只允许你绑定拒绝处理函数。
-
你能用多种方式将多个 Promise 串联在一起, 并在它们之间传递信息。 每个对 then() 的调 用都创建并返回了一个新的Promise, 在前一个 Promise 被决议时, 新 Promise 也会被决 议。 Promise
链可被用于触发对一系列异步事件的响应。 你还能使用 Promise.race() 与 Promise.all() 来监视多个
Promise 的进程, 并进行相应的响应。 -
组合使用生成器与 Promise 会让异步任务运行得更容易, 这是由于 Promise 提供了异步操作 可返回的一个通用接口。这样你就能使用生成器与 yield 运算符来等待异步响应, 并作出适当的应答。