11.1 异步编程

异步行为是为了优化因计算量大而时间长的操作。

11.1.1 同步与异步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

11.1.2 以往的异步编程模式

在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。
串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。

  1. function double(value) {
  2. setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
  3. }
  4. double(3);
  5. // 6(大约 1000 毫秒之后)

11.2 期约

11.2.1 Promises/A+规范

2012 年 Promises/A+组织fork了 CommonJS 的 Promises/A 建议,并以相同的名字制定了 Promises/A+规范。这个规范最终成为了
ECMAScript 6 规范实现的范本。

ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。
创建新期约时需要传入执行器(executor)函数作为参数

11.2.2 期约基础

ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。
创建新期约时需要传入执行器(executor)函数作为参数

11.2.2.1 期约状态机

期约是一个有状态的对象,可能处于如下 3 种状态之一

  • 待定 pending
  • 兑现 fulfilled/resloved
  • 拒绝 rejected

待定(pending)是期约的最初始状态。
在待定状态下,期约可以落定(settled)为代表成功的兑现(fulfilled)状态,或者代表失败的拒绝(rejected)状态。
无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,期约的状态就不再改变。而且,也不能保证期约必然会脱离待定状态。
因此,组织合理的代码无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该具有恰当的行为。

11.2.2.2 解决值、拒绝理由以及期约用例

期约主要有两大用途。首先是抽象地表示一个异步操作。期约的状态代表期约是否完成。
“待定”表示尚未开始或者正在执行中。“兑现”表示已经成功完成,而“拒绝”则表示没有成功完成。

期约封装的异步操作会实际生成某个值,而程序期待期约状态改变时可以访问这个值。
相应地,如果期约被拒绝,程序就会期待期约状态改变时可以拿到拒绝的理由。

11.2.2.3 通过执行函数控制期约状态

由于期约的状态是私有的,所以只能在内部进行操作。内部操作在期约的执行器函数中完成。
执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换。

这两个函数参数通常都命名为 resolve()和 reject()。
调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。

  1. let p1 = new Promise((resolve, reject) => resolve());
  2. setTimeout(console.log, 0, p1); // Promise <resolved>
  3. let p2 = new Promise((resolve, reject) => reject());
  4. setTimeout(console.log, 0, p2); // Promise <rejected>

11.2.2.4 Promise.reslove

期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。
通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。

这个解决的期约的值对应着传给 Promise.resolve()的第一个参数。
使用这个静态方法,实际上可以把任何值都转换为一个期约

  1. let p1 = new Promise((resolve, reject) => resolve());
  2. let p2 = Promise.resolve(); //等同于p1
  3. setTimeout(console.log, 0, Promise.resolve());
  4. // Promise <resolved>: undefined
  5. setTimeout(console.log, 0, Promise.resolve(3));
  6. // Promise <resolved>: 3
  7. // 多余的参数会忽略
  8. setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
  9. // Promise <resolved>: 4

11.2.2.5 Promise.reject

与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误

  1. let p1 = new Promise((resolve, reject) => reject());
  2. let p2 = Promise.reject(); //等同于p1
  3. let p = Promise.reject(3);
  4. setTimeout(console.log, 0, p); // Promise <rejected>: 3
  5. p.then(null, (e) => setTimeout(console.log, 0, e)); // 3

11.2.3 期约的实例方法

11.2.3.1 then

这个 then()方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。
这两个参数都是可选的,如果提供的话,则会在期约分别进入“兑现”和“拒绝”状态时执行。

  1. function onResolved(id) {
  2. setTimeout(console.log, 0, id, 'resolved');
  3. }
  4. function onRejected(id) {
  5. setTimeout(console.log, 0, id, 'rejected');
  6. }
  7. let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
  8. let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
  9. p1.then(() => onResolved('p1'), () => onRejected('p1'));
  10. p2.then(() => onResolved('p2'), () => onRejected('p2'));
  11. //(3 秒后)
  12. // p1 resolved
  13. // p2 rejected

11.2.3.2 catch

catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。

  1. let p = Promise.reject();
  2. let onRejected = function(e) {
  3. setTimeout(console.log, 0, 'rejected');
  4. };
  5. // 这两种添加拒绝处理程序的方式是一样的:
  6. p.then(null, onRejected); // rejected
  7. p.catch(onRejected); // rejected

11.2.3.3 finally

finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。
这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。

  1. let p1 = Promise.resolve();
  2. let p2 = Promise.reject();
  3. let onFinally = function() {
  4. setTimeout(console.log, 0, 'Finally!')
  5. }
  6. p1.finally(onFinally); // Finally
  7. p2.finally(onFinally); // Finally

11.2.4 期约连锁与期约合成

把期约逐个地串联起来是一种非常有用的编程模式。
因为每个期约实例的方法都会返回一个新的期约对象,而这个新期约又有自己的实例方法。

  1. let p = new Promise((resolve, reject) => {
  2. console.log('first');
  3. resolve();
  4. });
  5. p.then(() => console.log('second'))
  6. .then(() => console.log('third'))
  7. .then(() => console.log('fourth'));
  8. // first
  9. // second
  10. // third
  11. // fourth

11.2.4.1 Promise.all()

Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝
如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序

  1. let p = Promise.all([
  2. Promise.resolve(3),
  3. Promise.resolve(),
  4. Promise.resolve(4)
  5. ]);
  6. p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]

11.2.4.1 Promise.race()

Promise.race()静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约

Promise.race()不会对解决或拒绝的期约区别对待。
无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回新期约

11.2.5 期约扩展

11.3 异步函数

11.3.1 异步函数

ES8 的 async/await 旨在解决利用异步结构组织代码的问题。
为此,ECMAScript 对函数进行了扩展,为其增加了两个新关键字:async 和 await。

11.3.2 停止和恢复执行