Skip to content

期约与异步函数  #7

Open
Open
@webVueBlog

Description

@webVueBlog
  1. 异步编程
  2. 期约
  3. 异步函数

ECMAScript 6 新增了正式的 Promise(期约)引用类型

异步编程

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

同步与异步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值)。

以往的异步编程模式

俗称“回调地狱”

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

给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)

function double(value, callback) {
 setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${x}`));
// I was given: 6(大约 1000 毫秒之后)
  1. 失败处理
  2. 嵌套异步回调

期约

Promises/A+规范

期约基础

ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。

let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>

之所以说是应付解释器,是因为如果不提供执行器函数,就会抛出 SyntaxError。

  1. 期约状态机

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

  1. 待定(pending)
  2. 兑现(fulfilled,有时候也称为“解决”,resolved)
  3. 拒绝(rejected)

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

期约的状态是私有的,不能直接通过 JavaScript 检测到。这主要是为了避免根据读取到的期约状态,以同步方式处理期约对象。另外,期约的状态也不能被外部 JavaScript 代码修改。

  1. 解决值、拒绝理由及期约用例

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

每个期约只要状态切换为兑现,就会有一个私有的内部值(value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由(reason)。无论是值还是理由,都是包含原始值或对象的不可修改的引用。

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

内部操作在期约的执行器函数中完成。

主要有两项职责:初始化期约的异步行为和控制状态的最终转换。

控制期约状态的转换是通过调用它的两个函数参数实现的。这两个函数参数通常都命名为 resolve()和 reject()。调用resolve()会把状态切换为兑现,调用 reject()会把状态切换为拒绝。另外,调用 reject()也会抛出错误

let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 0, p1); // Promise <resolved>
let p2 = new Promise((resolve, reject) => reject());
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise)

Promise {<fulfilled>: undefined}

执行器函数是同步执行的。这是因为执行器函数是期约的初始化程序

new Promise(() => setTimeout(console.log, 0, 'executor'));
setTimeout(console.log, 0, 'promise initialized');
// executor
// promise initialized

添加 setTimeout 可以推迟切换状态

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
// 在 console.log 打印期约实例的时候,还不会执行超时回调(即 resolve())
setTimeout(console.log, 0, p); // Promise <pending> 
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
// 在 console.log 打印期约实例的时候,还不会执行超时回调(即 resolve())
setTimeout(console.log, 0, p); // Promise <pending> 

Promise {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: undefined

为避免期约卡在待定状态,可以添加一个定时退出功能。

let p = new Promise((resolve, reject) => {
 setTimeout(reject, 10000); // 10 秒后调用 reject()
 // 执行函数的逻辑
});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 11000, p); // 11 秒后再检查状态
// (After 10 seconds) Uncaught error
// (After 11 seconds) Promise <rejected>
  1. Promise.resolve()
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve();
setTimeout(console.log, 0, Promise.resolve());
// Promise <resolved>: undefined
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3
// 多余的参数会忽略
setTimeout(console.log, 0, Promise.resolve(4, 5, 6));
// Promise <resolved>: 4 

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p));
// true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p)));
// true

这个幂等性会保留传入期约的状态:

let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending>
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
let p = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo
  1. Promise.reject()
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();
let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3 

如果给它传一个期约对象,则这个期约会成为它返回的拒绝期约的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved> 
  1. 同步/异步执行的二元性
try {
 throw new Error('foo');
} catch(e) {
 console.log(e); // Error: foo
}
try {
 Promise.reject(new Error('bar'));
} catch(e) {
 console.log(e);
}
// Uncaught (in promise) Error: bar

第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。

期约的实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。

  1. 实现 Thenable 接口

在 ECMAScript 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了Thenable 接口。

class MyThenable {
 then() {}
}

ECMAScript 的 Promise 类型实现了 Thenable 接口。

  1. Promise.prototype.then()

Promise.prototype.then()是为期约实例添加处理程序的主要方法。这个 then()方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。

function onResolved(id) {
 setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
 setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
p1.then(() => onResolved('p1'),
 () => onRejected('p1'));
p2.then(() => onResolved('p2'),
 () => onRejected('p2'));
//(3 秒后)
// p1 resolved
// p2 rejected
function onResolved(id) {
 setTimeout(console.log, 0, id, 'resolved');
}
function onRejected(id) {
 setTimeout(console.log, 0, id, 'rejected');
}
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000));
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000));
// 非函数处理程序会被静默忽略,不推荐
p1.then('gobbeltygook');
// 不传 onResolved 处理程序的规范写法
p2.then(null, () => onRejected('p2'));
// p2 rejected(3 秒后)

Promise.prototype.then()方法返回一个新的期约实例:

let p1 = new Promise(() => {});
let p2 = p1.then();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false 
let p1 = Promise.resolve('foo');
// 若调用 then()时不传处理程序,则原样向后传
let p2 = p1.then(); 
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
// 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined 
// 这些都一样
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar
// Promise.resolve()保留返回的期约
let p8 = p1.then(() => new Promise(() => {}));
let p9 = p1.then(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined

抛出异常会返回拒绝的期约

let p10 = p1.then(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected> baz

注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中

let p11 = p1.then(() => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux

onRejected 处理程序的任务不就是捕获异步错误吗?因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。

let p1 = Promise.reject('foo');
// 调用 then()时不传处理程序则原样向后传
let p2 = p1.then();
// Uncaught (in promise) foo

setTimeout(console.log, 0, p2); // Promise <rejected>: foo
// 这些都一样
let p3 = p1.then(null, () => undefined);
let p4 = p1.then(null, () => {});
let p5 = p1.then(null, () => Promise.resolve());
setTimeout(console.log, 0, p3); // Promise <resolved>: undefined
setTimeout(console.log, 0, p4); // Promise <resolved>: undefined
setTimeout(console.log, 0, p5); // Promise <resolved>: undefined
// 这些都一样
let p6 = p1.then(null, () => 'bar');
let p7 = p1.then(null, () => Promise.resolve('bar'));
setTimeout(console.log, 0, p6); // Promise <resolved>: bar
setTimeout(console.log, 0, p7); // Promise <resolved>: bar
// Promise.resolve()保留返回的期约
let p8 = p1.then(null, () => new Promise(() => {}));
let p9 = p1.then(null, () => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p8); // Promise <pending>
setTimeout(console.log, 0, p9); // Promise <rejected>: undefined
let p10 = p1.then(null, () => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p10); // Promise <rejected>: baz
let p11 = p1.then(null, () => Error('qux'));
setTimeout(console.log, 0, p11); // Promise <resolved>: Error: qux 
  1. Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)。

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

Promise.prototype.catch()返回一个新的期约实例:

let p1 = new Promise(() => {});
let p2 = p1.catch();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false
  1. Promise.prototype.finally()

Promise.prototype.finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。

let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
 setTimeout(console.log, 0, 'Finally!')
}
p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally

Promise.prototype.finally()方法返回一个新的期约实例:

let p1 = new Promise(() => {});
let p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise <pending>
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(console.log, 0, p1 === p2); // false 

在大多数情况下它将表现为父期约的传递。对于已解决状态和被拒绝状态都是如此。

let p1 = Promise.resolve('foo');
// 这里都会原样后传
let p2 = p1.finally();
let p3 = p1.finally(() => undefined);
let p4 = p1.finally(() => {});
let p5 = p1.finally(() => Promise.resolve());
let p6 = p1.finally(() => 'bar');
let p7 = p1.finally(() => Promise.resolve('bar'));
let p8 = p1.finally(() => Error('qux'));
setTimeout(console.log, 0, p2); // Promise <resolved>: foo
setTimeout(console.log, 0, p3); // Promise <resolved>: foo
setTimeout(console.log, 0, p4); // Promise <resolved>: foo
setTimeout(console.log, 0, p5); // Promise <resolved>: foo
setTimeout(console.log, 0, p6); // Promise <resolved>: foo
setTimeout(console.log, 0, p7); // Promise <resolved>: foo
setTimeout(console.log, 0, p8); // Promise <resolved>: foo
// Promise.resolve()保留返回的期约
let p9 = p1.finally(() => new Promise(() => {}));
let p10 = p1.finally(() => Promise.reject());
// Uncaught (in promise): undefined
setTimeout(console.log, 0, p9); // Promise <pending>
setTimeout(console.log, 0, p10); // Promise <rejected>: undefined
let p11 = p1.finally(() => { throw 'baz'; });
// Uncaught (in promise) baz
setTimeout(console.log, 0, p11); // Promise <rejected>: baz

返回待定期约的情形并不常见,这是因为只要期约一解决,新期约仍然会原样后传初始的期约:

let p1 = Promise.resolve('foo');
// 忽略解决的值
let p2 = p1.finally(
 () => new Promise((resolve, reject) => setTimeout(() => resolve('bar'), 100)));
setTimeout(console.log, 0, p2); // Promise <pending>
setTimeout(() => setTimeout(console.log, 0, p2), 200);
// 200 毫秒后:
// Promise <resolved>: foo
  1. 非重入期约方法

当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处
理程序的代码之后的同步代码一定会在处理程序之前先执行。即使期约一开始就是与附加处理程序关联
的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy)
特性。

// 创建解决的期约
let p = Promise.resolve();
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler'));
// 同步输出,证明 then()已经返回
console.log('then() returns');
// 实际的输出:
// then() returns
// onResolved handler
let synchronousResolve;
// 创建一个期约并将解决函数保存在一个局部变量中
let p = new Promise((resolve) => {
 synchronousResolve = function() {
 console.log('1: invoking resolve()');
 resolve();
 console.log('2: resolve() returns');
 };
});
p.then(() => console.log('4: then() handler executes'));
synchronousResolve();
console.log('3: synchronousResolve() returns');
// 实际的输出:
// 1: invoking resolve()
// 2: resolve() returns
// 3: synchronousResolve() returns
// 4: then() handler executes

非重入适用于 onResolved/onRejected 处理程序、catch()处理程序和 finally()处理程序。

let p1 = Promise.resolve();
p1.then(() => console.log('p1.then() onResolved'));
console.log('p1.then() returns');
let p2 = Promise.reject();
p2.then(null, () => console.log('p2.then() onRejected'));
console.log('p2.then() returns');
let p3 = Promise.reject();
p3.catch(() => console.log('p3.catch() onRejected'));
console.log('p3.catch() returns');
let p4 = Promise.resolve();
p4.finally(() => console.log('p4.finally() onFinally'));
console.log('p4.finally() returns');
// p1.then() returns
// p2.then() returns
// p3.catch() returns
// p4.finally() returns
// p1.then() onResolved
// p2.then() onRejected
// p3.catch() onRejected
// p4.finally() onFinally 
  1. 邻近处理程序的执行顺序
let p1 = Promise.resolve();
let p2 = Promise.reject();
p1.then(() => setTimeout(console.log, 0, 1));
p1.then(() => setTimeout(console.log, 0, 2));
// 1
// 2
p2.then(null, () => setTimeout(console.log, 0, 3));
p2.then(null, () => setTimeout(console.log, 0, 4));
// 3
// 4
p2.catch(() => setTimeout(console.log, 0, 5));
p2.catch(() => setTimeout(console.log, 0, 6));
// 5
// 6
p1.finally(() => setTimeout(console.log, 0, 7));
p1.finally(() => setTimeout(console.log, 0, 8));
// 7
// 8 
  1. 传递解决值和拒绝理由

在执行函数中,解决的值和拒绝的理由是分别作为 resolve()和 reject()的第一个参数往后传的

这些值又会传给它们各自的处理程序,作为 onResolved 或 onRejected 处理程序的唯一参数。

let p1 = new Promise((resolve, reject) => resolve('foo'));
p1.then((value) => console.log(value)); // foo
let p2 = new Promise((resolve, reject) => reject('bar'));
p2.catch((reason) => console.log(reason)); // bar 

Promise.resolve()和 Promise.reject()在被调用时就会接收解决值和拒绝理由。它们返回的期约也会像执行器一样把这些值传给 onResolved 或 onRejected 处理程序

let p1 = Promise.resolve('foo');
p1.then((value) => console.log(value)); // foo
let p2 = Promise.reject('bar');
p2.catch((reason) => console.log(reason)); // bar
  1. 拒绝期约与拒绝错误处理

拒绝期约类似于 throw()表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理

let p1 = new Promise((resolve, reject) => reject(Error('foo')));
let p2 = new Promise((resolve, reject) => { throw Error('foo'); });
let p3 = Promise.resolve().then(() => { throw Error('foo'); });
let p4 = Promise.reject(Error('foo'));
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo
// 也会抛出 4 个未捕获错误

抛出的 4 个错误的栈追踪信息如下:

Uncaught (in promise) Error: foo
 at Promise (test.html:5)
 at new Promise (<anonymous>)
 at test.html:5
Uncaught (in promise) Error: foo
 at Promise (test.html:6)
 at new Promise (<anonymous>)
 at test.html:6
Uncaught (in promise) Error: foo
 at test.html:8
Uncaught (in promise) Error: foo
 at Promise.resolve.then (test.html:7)

所有错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息展示了错误发生的路径。

throw Error('foo');
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo

但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时
继续执行同步指令:

Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo 

异步错误只能通过异步的 onRejected 处理程序
捕获:

// 正确
Promise.reject(Error('foo')).catch((e) => {});
// 不正确

try {
 Promise.reject(Error('foo'));
} catch(e) {}

可以使用 try/catch 在执行函数
中捕获错误:

let p = new Promise((resolve, reject) => {
 try {
 throw Error('foo');
 } catch(e) {}
 resolve('bar');
});
setTimeout(console.log, 0, p); // Promise <resolved>: bar

then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。

console.log('begin synchronous execution');
try {
 throw Error('foo');
} catch(e) {
 console.log('caught error', e);
}
console.log('continue synchronous execution');
// begin synchronous execution
// caught error Error: foo
// continue synchronous execution
new Promise((resolve, reject) => {
 console.log('begin asynchronous execution');
 reject(Error('bar'));
}).catch((e) => {
 console.log('caught error', e);
}).then(() => {
 console.log('continue asynchronous execution');
});
// begin asynchronous execution
// caught error Error: bar
// continue asynchronous execution

期约连锁与期约合成

前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。

  1. 期约连锁

把期约逐个地串联起来是一种非常有用的编程模式

“期约连锁”

let p = new Promise((resolve, reject) => {
 console.log('first');
 resolve();
});
p.then(() => console.log('second'))
 .then(() => console.log('third'))
 .then(() => console.log('fourth'));
// first
// second
// third
// fourth

使用 4 个同步函数也可以做到:

(() => console.log('first'))();
(() => console.log('second'))();
(() => console.log('third'))();
(() => console.log('fourth'))(); 

串行化异步任务

let p1 = new Promise((resolve, reject) => {
 console.log('p1 executor');
 setTimeout(resolve, 1000);
});
p1.then(() => new Promise((resolve, reject) => {
 console.log('p2 executor');
 setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
 console.log('p3 executor');
 setTimeout(resolve, 1000);
 }))
 .then(() => new Promise((resolve, reject) => {
 console.log('p4 executor');
 setTimeout(resolve, 1000);
 }));
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

把生成期约的代码提取到一个工厂函数中

function delayedResolve(str) {
 return new Promise((resolve, reject) => {
 console.log(str);
 setTimeout(resolve, 1000);
 });
}

delayedResolve('p1 executor')
 .then(() => delayedResolve('p2 executor'))
 .then(() => delayedResolve('p3 executor'))
 .then(() => delayedResolve('p4 executor'))
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

每个后续的处理程序都会等待前一个期约解决,然后实例化一个新期约并返回它。

因为 then()、catch()和 finally()都返回期约

let p = new Promise((resolve, reject) => {
 console.log('initial promise rejects');
 reject();
});
p.catch(() => console.log('reject handler'))
 .then(() => console.log('resolve handler'))
 .finally(() => console.log('finally handler'));
// initial promise rejects
// reject handler
// resolve handler
// finally handler
  1. 期约图

因为一个期约可以有任意多个处理程序,所以期约连锁可以构建有向非循环图的结构。

这样,每个期约都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等待前一个节点落定,所以图的方向就是期约的解决或拒绝顺序。

展示了一种期约有向图,也就是二叉树:

// A
// / \
// B C
// /\ /\
// D E F G
let A = new Promise((resolve, reject) => {
 console.log('A');
 resolve();
});
let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));
B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));
// A
// B
// C
// D
// E
// F
// G

树只是期约图的一种形式。考虑到根节点不一定唯一,且多个期约也可以组合成一个期约

  1. Promise.all()和 Promise.race()

Promise 类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和 Promise.race()。而合成后期约的行为取决于内部期约的行为。

Promise.all()

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

let p1 = Promise.all([
 Promise.resolve(),
 Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于 Promise.resolve()
let p3 = Promise.all([]);
// 无效的语法
let p4 = Promise.all();
// TypeError: cannot read Symbol.iterator of undefined

合成的期约只会在每个包含的期约都解决之后才解决

let p = Promise.all([
 Promise.resolve(),
 new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p); // Promise <pending>
p.then(() => setTimeout(console.log, 0, 'all() resolved!'));
// all() resolved!(大约 1 秒后)

如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝:

// 永远待定
let p1 = Promise.all([new Promise(() => {})]);
setTimeout(console.log, 0, p1); // Promise <pending>
// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
 Promise.resolve(),
 Promise.reject(),
 Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught (in promise) undefined 

如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序

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

如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期
约不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。合成的期约会静默
处理所有包含期约的拒绝操作,如下所示:

// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.all([
 Promise.reject(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误

Promise.race()

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

let p1 = Promise.race([
 Promise.resolve(),
 Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve()转换为期约
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等价于 new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race();
// TypeError: cannot read Symbol.iterator of undefined

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

// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
 Promise.resolve(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3
// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
 Promise.reject(4),
 new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4
// 迭代顺序决定了落定顺序
let p3 = Promise.race([
 Promise.resolve(5),
 Promise.resolve(6),
 Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5 

如果有一个期约拒绝,只要它是第一个落定的,就会成为拒绝合成期约的理由。之后再拒绝的期约
不会影响最终期约的拒绝理由。不过,这并不影响所有包含期约正常的拒绝操作。与 Promise.all()
类似,合成的期约会静默处理所有包含期约的拒绝操作,如下所示:

// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.race([
 Promise.reject(3),
 new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误
  1. 串行期约合成

基于后续期约使用之前期约的返回值来串联期约是期约的基本功能。这

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
 return addFive(addTwo(addThree(x)));
}
console.log(addTen(7)); // 17
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
 return Promise.resolve(x)
 .then(addTwo)
 .then(addThree)
 .then(addFive);
}
addTen(8).then(console.log); // 18
使用 Array.prototype.reduce()可以写成更简洁的形式:
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
 return [addTwo, addThree, addFive]
 .reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18

使用 Array.prototype.reduce()

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function addTen(x) {
 return [addTwo, addThree, addFive]
 .reduce((promise, fn) => promise.then(fn), Promise.resolve(x));
}
addTen(8).then(console.log); // 18 
function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
function compose(...fns) {
 return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}
let addTen = compose(addTwo, addThree, addFive);
addTen(8).then(console.log); // 18

期约扩展

实际上,可以在现有实现基础上提供一种临时性的封装,以实现取消期约的功能。

class CancelToken {
 constructor(cancelFn) {
 this.promise = new Promise((resolve, reject) => {
 cancelFn(resolve);
 });
 }
} 

这个类包装了一个期约,把解决方法暴露给了 cancelFn 参数。这样,外部代码就可以向构造函数
中传入一个函数,从而控制什么情况下可以取消期约。这里期约是令牌类的公共成员,因此可以给它添
加处理程序以取消期约。

<button id="start">Start</button>
<button id="cancel">Cancel</button>
<script>
class CancelToken {
 constructor(cancelFn) {
 this.promise = new Promise((resolve, reject) => {
 cancelFn(() => {
 setTimeout(console.log, 0, "delay cancelled");
 resolve();
 });
 });
 }
}
const startButton = document.querySelector('#start');
const cancelButton = document.querySelector('#cancel');
function cancellableDelayedResolve(delay) {
 setTimeout(console.log, 0, "set delay");
 return new Promise((resolve, reject) => {
 const id = setTimeout((() => {
 setTimeout(console.log, 0, "delayed resolve");
 resolve();
 }), delay); 
const cancelToken = new CancelToken((cancelCallback) =>
 cancelButton.addEventListener("click", cancelCallback));
 cancelToken.promise.then(() => clearTimeout(id));
 });
}
startButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>

每次单击“Start”按钮都会开始计时,并实例化一个新的 CancelToken 的实例。此时,“Cancel”
按钮一旦被点击,就会触发令牌实例中的期约解决。而解决之后,单击“Start”按钮设置的超时也会被
取消。

  1. 期约进度通知

执行中的期约可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控期
约的执行进度会很有用。ECMAScript 6 期约并不支持进度追踪,但是可以通过扩展来实现。
一种实现方式是扩展 Promise 类,为它添加 notify()方法,如下所示:

class TrackablePromise extends Promise {
 constructor(executor) {
 const notifyHandlers = [];
 super((resolve, reject) => {
 return executor(resolve, reject, (status) => {
 notifyHandlers.map((handler) => handler(status));
 });
 });
 this.notifyHandlers = notifyHandlers;
 }
 notify(notifyHandler) {
 this.notifyHandlers.push(notifyHandler);
 return this;
 }
} 

这样,TrackablePromise 就可以在执行函数中使用 notify()函数了。可以像下面这样使用这个
函数来实例化一个期约:

let p = new TrackablePromise((resolve, reject, notify) => {
 function countdown(x) {
 if (x > 0) {
 notify(`${20 * x}% remaining`);
 setTimeout(() => countdown(x - 1), 1000);
 } else {
 resolve();
 }
 }
 countdown(5);
});

这个期约会连续5次递归地设置1000毫秒的超时。每个超时回调都会调用notify()并传入状态值。
假设通知处理程序简单地这样写:

let p = new TrackablePromise((resolve, reject, notify) => {
 function countdown(x) {
 if (x > 0) {
 notify(`${20 * x}% remaining`);
 setTimeout(() => countdown(x - 1), 1000);
 } else {
 resolve();
 }
 }
 countdown(5);
});
p.notify((x) => setTimeout(console.log, 0, 'progress:', x));
p.then(() => setTimeout(console.log, 0, 'completed'));
// (约 1 秒后)80% remaining
// (约 2 秒后)60% remaining
// (约 3 秒后)40% remaining
// (约 4 秒后)20% remaining
// (约 5 秒后)completed 

notify()函数会返回期约,所以可以连缀调用,连续添加处理程序。多个处理程序会针对收到的
每条消息分别执行一遍,如下所示:

p.notify((x) => setTimeout(console.log, 0, 'a:', x))
.notify((x) => setTimeout(console.log, 0, 'b:', x));
p.then(() => setTimeout(console.log, 0, 'completed'));
// (约 1 秒后) a: 80% remaining
// (约 1 秒后) b: 80% remaining
// (约 2 秒后) a: 60% remaining
// (约 2 秒后) b: 60% remaining
// (约 3 秒后) a: 40% remaining
// (约 3 秒后) b: 40% remaining
// (约 4 秒后) a: 20% remaining
// (约 4 秒后) b: 20% remaining
// (约 5 秒后) completed

总体来看,这还是一个比较粗糙的实现,但应该可以演示出如何使用通知报告进度了。

异步函数

异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。
async/await 是 ES8 规范新增的。这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码
能够异步执行

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
p.then((x) => console.log(x)); // 3

function handler(x) { console.log(x); }
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
p.then(handler); // 3

异步函数

ES8 的 async/await 旨在解决利用异步结构组织代码的问题。

  1. async

async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:

async function foo() {}
let bar = async function() {};
let baz = async () => {};
class Qux {
 async qux() {}
}

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭
包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。正如下面的例子所示,foo()函数仍然会
在后面的指令之前被求值:

async function foo() {
 console.log(1);
}
foo();
console.log(2);
// 1
// 2

不过,异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这
个值会被 Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。

async function foo() {
 console.log(1);
 return 3;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3
当然,直接返回一个期约对象也是一样的:
async function foo() {
 console.log(1);
 return Promise.resolve(3);
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3 

异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。
如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then()的处理程序“解包”。如果
不是,则返回值就被当作已经解决的期约。

// 返回一个原始值
async function foo() {
 return 'foo';
}
foo().then(console.log);
// foo
// 返回一个没有实现 thenable 接口的对象
async function bar() {
 return ['bar'];
}
bar().then(console.log);
// ['bar']
// 返回一个实现了 thenable 接口的非期约对象
async function baz() {
 const thenable = {
 then(callback) { callback('baz'); }
 };
 return thenable;
}
baz().then(console.log);
// baz
// 返回一个期约
async function qux() {
 return Promise.resolve('qux');
}
qux().then(console.log);
// qux
与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:
async function foo() {
 console.log(1);
 throw 3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
不过,拒绝期约的错误不会被异步函数捕获:
async function foo() {
 console.log(1);
 Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught (in promise): 3 
  1. await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await
关键字可以暂停异步函数代码的执行,等待期约解决

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
p.then((x) => console.log(x)); // 3
使用 async/await 可以写成这样:
async function foo() {
 let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
 console.log(await p);
}
foo();
// 3

注意,await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行
为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这
个值传给表达式,再异步恢复异步函数的执行。

await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用

// 异步打印"foo"
async function foo() {
 console.log(await Promise.resolve('foo'));
}
foo();
// foo
// 异步打印"bar"
async function bar() {
 return await Promise.resolve('bar');
}
bar().then(console.log);
// bar
// 1000 毫秒后异步打印"baz"
async function baz() {
 await new Promise((resolve, reject) => setTimeout(resolve, 1000));
 console.log('baz');
}
baz();
// baz(1000 毫秒后)

await 关键字期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如
果是实现 thenable 接口的对象,则这个对象可以由 await 来“解包”。

// 等待一个原始值
async function foo() {
 console.log(await 'foo');
}
foo();
// foo
// 等待一个没有实现 thenable 接口的对象
async function bar() {
 console.log(await ['bar']);
}
bar();
// ['bar']
// 等待一个实现了 thenable 接口的非期约对象
async function baz() {
 const thenable = {
 then(callback) { callback('baz'); }
 };
 console.log(await thenable);
}
baz();
// baz
// 等待一个期约
async function qux() {
 console.log(await Promise.resolve('qux'));
}
qux();
// qux
等待会抛出错误的同步操作,会返回拒绝的期约:
async function foo() {
 console.log(1);
 await (() => { throw 3; })();
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

单独的 Promise.reject()不会被异步函数捕获,而会抛出未捕获错误

async function foo() {
 console.log(1);
 await Promise.reject(3);
 console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
  1. await 的限制

await 关键字必须在异步函数中使用,不能在顶级上下文如<script>标签或模块中使用。不过,
定义并立即调用异步函数是没问题的。

async function foo() {
 console.log(await Promise.resolve(3));
}
foo();
// 3
// 立即调用的异步函数表达式
(async function() {
 console.log(await Promise.resolve(3));
})();
// 3 

await 关键字也只能直接出现在异步函数的定
义中。在同步函数内部使用 await 会抛出 SyntaxError。

// 不允许:await 出现在了箭头函数中
function foo() {
 const syncFn = () => {
 return await Promise.resolve('foo');
 };
 console.log(syncFn());
}
// 不允许:await 出现在了同步函数声明中
function bar() {
 function syncFn() {
 return await Promise.resolve('bar');
 }
 console.log(syncFn());
}
// 不允许:await 出现在了同步函数表达式中
function baz() {
 const syncFn = function() {
 return await Promise.resolve('baz');
 };
 console.log(syncFn());
}
// 不允许:IIFE 使用同步函数表达式或箭头函数
function qux() {
 (function () { console.log(await Promise.resolve('qux')); })();
 (() => console.log(await Promise.resolve('qux')))();

停止和恢复执行

async function foo() {
 console.log(await Promise.resolve('foo'));
}
async function bar() {
 console.log(await 'bar');
}
async function baz() {
 console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。

async function foo() {
 console.log(2);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3

JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。

async function foo() {
 console.log(2);
 await null;
 console.log(4);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
// 4
控制台中输出结果的顺序很好地解释了运行时的工作过程:
(1) 打印 1
(2) 调用异步函数 foo()
(3)(在 foo()中)打印 2
(4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;
(5) foo()退出;
(6) 打印 3
(7) 同步线程的代码执行完毕;
(8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行;
(9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用);
(10)(在 foo()中)打印 4
(11) foo()返回。
async function foo() {
 console.log(2);
 console.log(await Promise.resolve(8));
 console.log(9);
}
async function bar() {
 console.log(4);
 console.log(await 6);
 console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
运行时会像这样执行上面的例子:
(1) 打印 1
(2) 调用异步函数 foo()
(3)(在 foo()中)打印 2
(4)(在 foo()中)await 关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;
(5) 期约立即落定,把给 await 提供值的任务添加到消息队列;
(6) foo()退出;
(7) 打印 3
(8) 调用异步函数 bar()
(9)(在 bar()中)打印 4
(10)(在 bar()中)await 关键字暂停执行,为立即可用的值 6 向消息队列中添加一个任务;
(11) bar()退出;
(12) 打印 5
(13) 顶级线程执行完毕;
(14) JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给它;
(15) JavaScript 运行时向消息队列中添加一个恢复执行 foo()函数的任务;
(16) JavaScript 运行时从消息队列中取出恢复执行 bar()的任务及值 6
(17)(在 bar()中)恢复执行,await 取得值 6
(18)(在 bar()中)打印 6
(19)(在 bar()中)打印 7
(20) bar()返回;
(21) 异步任务完成,JavaScript 从消息队列中取出恢复执行 foo()的任务及值 8
(22)(在 foo()中)打印 8
(23)(在 foo()中)打印 9
(24) foo()返回。

异步函数策略

  1. 实现 sleep()

很多人在刚开始学习 JavaScript 时,想找到一个类似 Java 中 Thread.sleep()之类的函数,好在程
序中加入非阻塞的暂停。以前,这个需求基本上都通过 setTimeout()利用 JavaScript 运行时的行为来
实现的。

有了异步函数之后,就不一样了。一个简单的箭头函数就可以实现 sleep():

async function sleep(delay) {
 return new Promise((resolve) => setTimeout(resolve, delay));
}
async function foo() {
 const t0 = Date.now();
 await sleep(1500); // 暂停约 1500 毫秒
 console.log(Date.now() - t0);
}
foo();
// 1502 
  1. 利用平行执行

如果使用 await 时不留心,则很可能错过平行加速的机会。来看下面的例子,其中顺序等待了 5
个随机的超时:

async function randomDelay(id) {
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
 console.log(`${id} finished`);
 resolve();
 }, delay));
}
async function foo() {
 const t0 = Date.now();
 await randomDelay(0);
 await randomDelay(1);
 await randomDelay(2);
 await randomDelay(3);
 await randomDelay(4);
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed

用一个 for 循环重写,就是:

async function randomDelay(id) {
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
 console.log(`${id} finished`);
 resolve();
 }, delay));
}
async function foo() {
 const t0 = Date.now();
 for (let i = 0; i < 5; ++i) {
 await randomDelay(i);
 }
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 0 finished
// 1 finished
// 2 finished
// 3 finished
// 4 finished
// 877ms elapsed

就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序,
但总执行时间会变长。
如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:

async function randomDelay(id) {
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
 setTimeout(console.log, 0, `${id} finished`);
 resolve();
 }, delay));
}
async function foo() {
 const t0 = Date.now();
 const p0 = randomDelay(0);
 const p1 = randomDelay(1);
 const p2 = randomDelay(2);
 const p3 = randomDelay(3);
 const p4 = randomDelay(4);
 await p0;
 await p1;
 await p2;
 await p3;
 await p4;
 setTimeout(console.log, 0, `${Date.now() - t0}ms elapsed`);
}
foo();
// 1 finished 
// 4 finished
// 3 finished
// 0 finished
// 2 finished
// 877ms elapsed 
用数组和 for 循环再包装一下就是:
async function randomDelay(id) {
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
 console.log(`${id} finished`);
 resolve();
 }, delay));
}
async function foo() {
 const t0 = Date.now();
 const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
 for (const p of promises) {
 await p;
 }
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo();
// 4 finished
// 2 finished
// 1 finished
// 0 finished
// 3 finished
// 877ms elapsed
注意,虽然期约没有按照顺序执行,但 await 按顺序收到了每个期约的值:
async function randomDelay(id) {
 // 延迟 0~1000 毫秒
 const delay = Math.random() * 1000;
 return new Promise((resolve) => setTimeout(() => {
 console.log(`${id} finished`);
 resolve(id);
 }, delay));
}
async function foo() {
 const t0 = Date.now();
 const promises = Array(5).fill(null).map((_, i) => randomDelay(i));
 for (const p of promises) {
 console.log(`awaited ${await p}`);
 }
 console.log(`${Date.now() - t0}ms elapsed`);
}
foo(); 
// 1 finished
// 2 finished
// 4 finished
// 3 finished
// 0 finished
// awaited 0
// awaited 1
// awaited 2
// awaited 3
// awaited 4
// 645ms elapsed
  1. 串行执行期约

我们讨论过如何串行执行期约并把值传给后续的期约。使用 async/await,期约连锁会变
得很简单:

function addTwo(x) {return x + 2;}
function addThree(x) {return x + 3;}
function addFive(x) {return x + 5;}
async function addTen(x) {
 for (const fn of [addTwo, addThree, addFive]) {
 x = await fn(x);
 }
 return x;
}
addTen(9).then(console.log); // 19

这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约,
如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:

async function addTwo(x) {return x + 2;}
async function addThree(x) {return x + 3;}
async function addFive(x) {return x + 5;}
async function addTen(x) {
 for (const fn of [addTwo, addThree, addFive]) {
 x = await fn(x);
 }
 return x;
}
addTen(9).then(console.log); // 19
  1. 栈追踪与内存管理

期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。看看下面的例子,
它展示了拒绝期约的栈追踪信息:

function fooPromiseExecutor(resolve, reject) {
 setTimeout(reject, 1000, 'bar');
}
function foo() {
 new Promise(fooPromiseExecutor);
} 
foo();
// Uncaught (in promise) bar
// setTimeout
// setTimeout (async)
// fooPromiseExecutor
// foo

根据对期约的不同理解程度,以上栈追踪信息可能会让某些读者不解。栈追踪信息应该相当直接地
表现 JavaScript 引擎当前栈内存中函数调用之间的嵌套关系。在超时处理程序执行时和拒绝期约时,我
们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。可是,我们知道这
些函数已经返回了,因此栈追踪信息中不应该看到它们。

答案很简单,这是因为 JavaScript 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时,
调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息
会占用内存,从而带来一些计算和存储成本。

如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

function fooPromiseExecutor(resolve, reject) {
 setTimeout(reject, 1000, 'bar');
}
async function foo() {
 await new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
// foo
// async function (async)
// foo

这样一改,栈追踪信息就准确地反映了当前的调用栈。fooPromiseExecutor()已经返回,所以
它不在错误信息中。但 foo()此时被挂起了,并没有退出。JavaScript 运行时可以简单地在嵌套函数中
存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上存储在内存中,可用于在出
错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以
优先考虑的。

🆗

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions