异步编程
在 JavaScript 这种单线程事件循环模型
中,同步操作与异步操作是代码所要依赖的核心机制。
异步行为是为了优化因计算量大而时间长的操作。在等待其他操作完成时,即使运行其他指令,系统也能保持稳定。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。
以往的异步编程(setTimeout)
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value * 2), 1000);
}
double(3);
// 6(大约 1000 毫秒之后)
对这个例子而言,1000 毫秒之后,JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后,回调什么时候出列被执行对 JavaScript 代码就完全不可见了。double()函数在 setTimeout 成功调度异步操作之后会立即退出。
1.处理异步返回值
假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?
=》给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, (x) => console.log(`I was given: ${
x}`));
// I was given: 6(大约 1000 毫秒之后)
这里的 setTimeout 调用告诉 JavaScript 运行时在 1000 毫秒之后把一个函数推到消息队列上。这个函数会由运行时负责异步调度执行。而位于函数闭包中的回调及其参数在异步执行时仍然是可用的。
2.失败处理
成功回调和失败回调:
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(2 * value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => console.log(`Success: ${
x}`);
const failureCallback = (e) => console.log(`Failure: ${
e}`);
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)
这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。
3.嵌套异步回调
如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这
就要求嵌套回调:
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(2 * value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => {
double(x, (y) => console.log(`Success: ${
y}`));
};
const failureCallback = (e) => console.log(`Failure: ${
e}`);
double(3, successCallback, failureCallback);
// Success: 12(大约 1000 毫秒之后)
显然,随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱
”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。
Promise
成为了主导性的异步编程机制。
Promise 基础
ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。
当通过new
创建Promise
实例时,需要传入一个回调函数,我们称之为executor
。
- 这个回调函数会被立刻执行,并传入两个回调参数
resolve
、reject
- 当调用
resolve
回调函数时,会执行 Promise 对象的then
方法传入的回调 - 当调用
reject
回调函数时,会执行 Promise 对象的catch
方法传入的回调
promise 是一个有状态的对象,有以下3种:
待定(
pending
):最初始状态代表尚未开始或者正在执行中
兑现(fulfilled
,有时候也称为“解决”,resolved
):代表成功
拒绝(rejected
):代表失败
无论落定为哪种状态都是不可逆的。
只要从待定转换为兑现或拒绝,promise的状态就不再改变。
重要的是,promise的状态是私有的,不能直接通过 JavaScript 检测到或修改。promise故意将异步行为封装起来,从而隔离外部的同步代码。
2.解决值、拒绝理由及用例
promise主要有两大用途。
(1)抽象地表示一个异步操作。
(2)promise封装的异步操作会实际生成某个值,而程序期待promise状态改变时可以访问这个值。
为了支持这两种用例,每个promise只要状态切换为resolved,就会有一个私有的内部值(value)。
每个promise只要状态切换为rejected,就会有一个私有的内部理由(reason)。
3.通过执行函数控制promise状态
由于promise的状态是私有的,所以只能在内部进行操作。内部操作在promise的执行器函数
中完成。
执行器函数主要有两项职责:初始化promise的异步行为和控制状态的最终转换。
其中,
控制promise状态的转换
是通过调用它的两个函数参数实现的。调用
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时,执行器函数已经改变了每个promise的状态。这里的关键在于,执行器函数是同步执行的。这是因为执行器函数是promise的初始化程序
。
通过下面的例子可以看出上面代码的执行顺序:
new Promise((