title: JavaScript高级程序设计第四版学习–第十一章
date: 2021-5-23 19:42:09
author: Xilong88
tags: JavaScript
本章内容
异步编程
期约
异步函数
可能出现的面试题:
1.了解过Promise吗?
2.Promise有几种状态机?
3.Promise的常见方法?
4.如何实现期约取消?
5.如何实现进度追踪?
6.了解过异步函数吗?
7.性能优化,await和Promise的错误追踪的性能。
总结:
这一章主要就是讲Promise,async,await,这些可以把异步事件变得很优雅。
知识点:
1.在早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。
2.期约 Promise(ES6)
可以通过new 操作符来实例化。创建新期约时需要传入执行器(executor)函数作为参数。
let p = new Promise(() => {});
setTimeout(console.log, 0, p); // Promise <pending>
3.执行器函数
这是因为执行器函数是期约的初始化程序。也就是说初始化期约实例的时候,会执行。
4.期约的状态
待定(pending)
兑现(fulfilled,有时候也称为“解决”resolved)
拒绝(rejected)
每个期约只要状态切换为兑现,就会有一个
私有的内部值 (value)。类似地,每个期约只要状态切换为拒绝,就会有一个私有的内部理由 (reason)。
5.resolve() 和reject()
resolve() 和reject() 中的哪个被调用,状态转换都不可撤销了,继续修改状态会静默失败
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 没有效果
});
setTimeout(console.log, 0, p); // Promise <resolved>
为避免期约卡在待定状态,可以添加一个定时退出功能。比如,可
以通过setTimeout 设置一个10秒钟后无论如何都会拒绝期约的回
调:
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>
因为期约的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让期约处于待定状态的最长时间。如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败。
6.Promise.resolve()
通过调用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
假如传入一个期约,那么会返回该期约,保留期约的状态,并且无论调用多少次都一样。
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 = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p);
// Promise <resolved>: Error: foo
7.Promise.reject()
Promise.reject() 会实例化一个拒绝的期约并抛出一个异步错误(这个错误不能通过try /catch 捕获,而只能通过拒绝处理程序捕获)
拒绝的理由就是传入的第一个参数,就算传入一个期约,这个期约也会成为拒绝的理由
setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// Promise <rejected>: Promise <resolved>
8.同步/异步执行的二元性
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 块并不能捕获该错误。
9.期约的实例方法
Promise.prototype.then()
异步结构中,任何对象都被认为实现了Thenable接口,也就是说能使用.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
也就是会根据期约的状态来选择执行哪个方法。
因为期约只能转换为最终状态一次,所以这两个操作一定是互斥的。
给then传入任何非函数的数据类型都会被静默忽略。
如果只希望传入onRejected方法,那么需要把第一个参数设为undefined或者null。这样可以减少内存占用,因为少创建一个对象。
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秒后)
then的返回值是一个新的期约实例。
这个新期约实例基于onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过Promise.resolve() 包装来生成新期约。如果没有提供这个处理程序,则Promise.resolve() 就会包装上一个期约解决之后的值。如果没有显式的返回语句,则Promise.resolve() 会包装默认的返回值undefined 。
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
如果有显式的返回值,则Promise.resolve() 会包装这个值:
...
// 这些都一样
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
也就是说,返回一个错误值和抛出一个错误值,概念是不一样的。
Promise.reject()
onRejected 处理程序返回的值也会被Promise.resolve() 包装。
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
Promise.prototype.catch()
Promise.prototype.catch() 方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。
其实相当于在then之后去调用了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
在返回新期约实例方面Promise.prototype.catch() 的行为与Promise.prototype.then() 的onRejected处理程序是一样的
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
如果返回的是一个待定的期约,或者onFinally 处理程序抛出了错误(显式抛出或返回了一个拒绝期约),则会返回相应的期约(待定或拒绝),如下所示:
...
// 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
10.非重入期约方法
当期约进入落定状态时,与该状态相关的处理程序仅仅会被排期,而非立即执行。跟在添加这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。
// 创建解决的期约
let p = Promise.resolve();
// 添加解决处理程序
// 直觉上,这个处理程序会等期约一解决就执行
p.then(() => console.log('onResolved handler'));
// 同步输出,证明then()已经返回
console.log('then() returns');
// 实际的输出:
// then() returns
// onResolved handler
先添加处理程序后解决期约也是一样的。如果添加处理程序后,同步代码才改变期约状态,那么处理程序仍然会基于该状态变化表现出非重入特性。下面的例子展示了即使先添加了onResolved 处理程序,再同步调用resolve() ,处理程序也不会进入同步线程执行:
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
11.邻近处理程序的执行顺序
如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是then() 、catch() 还是finally() 添加的处理程序都是如此。
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
12.传递解决值和拒绝理由
到了落定状态后,期约会提供其解决值(如果兑现)或其拒绝理由如果拒绝)给相关状态的处理程序。拿到返回值后,就可以进一步对这个值进行操作。
在执行函数中,解决的值和拒绝的理由是分别作为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
13.拒绝期约与拒绝错误处理
拒绝期约类似于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个未捕获错误
期约可以以任何理由拒绝,包括undefined ,但最好统一使用错误对象。例如,前面例子中抛出的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() 关键字抛出错误时,JavaScript运行时的错误处理机制会停止执行抛出错误之后的任何指令:
throw Error('foo');
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo
但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令:
Promise.reject(Error('foo'));
console.log('bar');
// bar
// Uncaught (in promise) Error: foo
如本章前面的Promise.reject() 示例所示,异步错误只能通过异步的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
14.期约连锁与期约合成
多个期约组合在一起可以构成强大的代码逻辑。这种组合可以通过两种方式实现:期约连锁与期约合成。前者就是一个期约接一个期约地拼接,后者则是将多个期约组合为一个期约。
期约连锁
因为每个期约实例的方法(then() 、catch() 和finally() )都会返回一个新的 期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成所谓的“期约连锁”。比如:
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秒后)
假如这种情况下不使用期约,那么前面的代码可
能就要这样写了:
function delayedExecute(str, callback = null) {
setTimeout(() => {
console.log(str);
callback && callback();
}, 1000)
}
delayedExecute('p1 callback', () => {
delayedExecute('p2 callback', () => {
delayedExecute('p3 callback', () => {
delayedExecute('p4 callback');
});
});
});
// p1 callback(1秒后)
// p2 callback(2秒后)
// p3 callback(3秒后)
// p4 callback(4秒后)
因为then() 、catch() 和finally() 都返回期约,所以串联这些方法也很直观。下面的例子同时使用这3个实例方法:
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
15.期约图
下面的例子展示了一种期约有向图,也就是二叉树:
// 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
16.Promise.all() 和Promise.race()
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(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() 静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约:
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
合成的期约会 静默处理所有包含期约的拒绝操作,如下所示:
// 虽然只有第一个期约的拒绝理由会进入
// 拒绝处理程序,第二个期约的拒绝也
// 会被静默处理,不会有错误跑掉
let p = Promise.race([
Promise.reject(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
p.catch((reason) => setTimeout(console.log, 0, reason)); // 3
// 没有未处理的错误
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
用合成函数可以更方便:
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
18.期约扩展
期约取消和进度追踪
期约取消:ES6没有原生的期约取消。
使用“令牌”模式,可以实现:
CancelToken 类
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(resolve);
});
}
}
<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”按钮设置的超时
也会被取消。
原理就是说实例化时暴露了一个接口,这个接口可以提前resolve()期约。
期约进度通知
ES6没有原生的进度通知
执行中的期约可能会有不少离散的“阶段”,在最终解决之前必须依次经过。
某些情况下,监控期约的执行进度会很有用。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);
});
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
以上是一种实现进度通知的方法。
19.异步函数
async/await是ES8规范新增的
async
async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:
async function foo() {}
let bar = async function() {};
let baz = async () => {};
class Qux {
async qux() {}
}
这个关键字就表示,该函数有异步特征,但是内部真的有没有也不知道,所以仍然是具有普通函数的特征的,也就是执行依然是同步执行。
async function foo() {
console.log(1);
}
foo();
console.log(2);
// 1
// 2
异步函数的返回值会被包装成一个期约对象,没有就包装undefined对象,返回期约也一样。
// 返回一个原始值
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
await
await 关键字可以暂停异步函数代码的执行,等待期约解决。
async function foo() {
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
console.log(await p);
}
foo();
// 3
await 关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。这个行为与生成器函数中的yield 关键字是一样的。
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() 不会被异步函数捕获,而会抛出未捕获错误。不过,对拒绝的期约使用await 则会释放(unwrap)错误值(将拒绝期约返回):
async function foo() {
console.log(1);
await Promise.reject(3);
console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
await 的限制
await 关键字必须在异步函数中使用,不能在顶级上下文如
此外,异步函数的特质不会扩展到嵌套函数。因此,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 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
20.异步函数策略
实现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
利用平行执行
也就是说我们不关心await后面的期约的情况下,可以一次性初始化全部期约,然后挨个await,这样可以节约时间。
初始化一个等一个:
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
一起初始化:
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
串行执行期约
通过await可以把期约串行起来而不用then方法
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
非常方便
栈追踪与内存管理
用期约和用await的报错的栈追踪信息,后者更小。
function fooPromiseExecutor(resolve, reject) {
setTimeout(reject, 1000, 'bar');
}
function foo() {
new Promise(fooPromiseExecutor);
}
foo();
// Uncaught (in promise) bar
// setTimeout
// setTimeout (async)
// fooPromiseExecutor
// foo
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
这样可以优化性能。