11.1 异步编程
异步行为是为了优化因计算量大而时间长的操作。
11.1.1 同步与异步
同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。
异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。
11.1.2 以往的异步编程模式
在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。
串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称“回调地狱”)来解决。
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
}
double(3);
// 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()会把状态切换为拒绝。
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>
11.2.2.4 Promise.reslove
期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。
通过调用Promise.resolve()静态方法,可以实例化一个解决的期约。
这个解决的期约的值对应着传给 Promise.resolve()的第一个参数。
使用这个静态方法,实际上可以把任何值都转换为一个期约
let p1 = new Promise((resolve, reject) => resolve());
let p2 = Promise.resolve(); //等同于p1
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
11.2.2.5 Promise.reject
与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误
let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject(); //等同于p1
let p = Promise.reject(3);
setTimeout(console.log, 0, p); // Promise <rejected>: 3
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3
11.2.3 期约的实例方法
11.2.3.1 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
11.2.3.2 catch
catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。
let p = Promise.reject();
let onRejected = function(e) {
setTimeout(console.log, 0, 'rejected');
};
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected
11.2.3.3 finally
finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。
这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。
let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
setTimeout(console.log, 0, 'Finally!')
}
p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally
11.2.4 期约连锁与期约合成
把期约逐个地串联起来是一种非常有用的编程模式。
因为每个期约实例的方法都会返回一个新的期约对象,而这个新期约又有自己的实例方法。
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
11.2.4.1 Promise.all()
Promise.all()静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约
如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝
如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);
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。