异步编程
JS 引擎使用单线程执行代码,某一时刻只能执行一段代码,不能并行执行代码。执行代码时,JS 引擎会把将要执行的代码段放入作业队列中,当前代码执行完毕后,会从队列中取出代码段并执行,循环上述操作,直到执行完所有需执行的代码。
每个事件与一个处理该事件的代码段关联,一般情况下这个代码段组织成函数。当事件发生时,该代码段被添加到作业队列中,作业队列中前面的作业都执行完毕后,执行该代码段从而处理关联的事件。事件与处理程序必须在该事件发生之前绑定,不然事件发生时该程序不会执行。
回调函数是一个代码段,作为参数传入一个函数中,该函数在执行过程中因为某种条件未满足需要等待时,js 引擎不会等待该函数,而是会继续执行该函数下面的语句。当条件满足后,该函数恢复执行。一般情况下条件满足时执行回调函数。例如,一个读取磁盘文件的函数执行时,需要等待读取完成(不论成功还是失败)才能继续执行。当调用其他程序读取磁盘时,该函数需要等待,直到返回读取结果。结果返回后,该函数才能接着执行。在其他程序读取磁盘过程中,js 引擎会执行该函数下面的语句。读取磁盘的结果返回时,回调函数及作为其参数的返回结果将被加入到作业队列中,后续由 js 引擎执行完队列中前面的所有作业后将其从队列中取出执行。
Promise 基础
Promise 表示一个异步操作。异步操作没有完成时,Promise 的状态为挂起态 (pending state),称为 unsettled;异步操作完成时,称为 settled,此时 Promise 有两种可能的状态:
- 已完成:异步操作成功结束。
- 已拒绝:因某种原因,异步操作未完成结束。
Promise 的完整生命周期为 unsettled 到 settled,合计三种状态。允许执行的操作取决于 Promise 的当前状态。具体三种状态由 [[PromiseState]] 属性设置,该属性不可读写。
Promise 的 then()
方法接收两个可选的参数,这两个参数都是函数,当 Promise 的状态为已完成时,第一个参数执行,异步操作成功结束的结果作为一个参数传入该函数;当 Promise 的状态为已拒绝时,第二个参数执行,异步操作未成功结束的结果作为一个参数传入该函数。使用上述方式实现 then()
的任何对象都称为一个 thenable,Promise 是 thenable,反之不然。
let promise = asyFun(); // 执行一个有异步操作的函数,下面是三种处理情形。
promise.then((content) => {
console.log(content);
}, err => {
console.error(err.message);
});
promise.then(content => {}); // 传入前面的参数
promise.then(null, err => {}); // 传入后面的参数
Promise 的 catch()
在状态为已拒绝时执行,相当于 then()
仅传入后一个参数的情形。
Promise 允许在调用处理函数时,再次创建 Promise 的处理函数。这个时候,新的处理函数被添加到作业队列中,前面的作业处理完毕后,调用该处理函数。Promise 使用一个单独的作业队列,只添加 Promise 相关的处理函数。每次调用 then()
或 catch()
时,会创建新的作业,Promise 为 settled 时,从队列中取出执行。
promise.then(content => {
console.log(content);
promise.then(content => {},
err => {});
});
Promise 的构造器有一个参数,是一个称为执行器的函数,里面包含初始化 Promise 的代码。执行器有两个参数,名称分别为 resolve 和 reject。这两个参数也是函数,当执行器成功完成时,resolve()
被调用;未成功完成时,reject()
被调用。通常执行器里的代码是一个异步操作,异步操作的结果决定是 resolve()
还是 reject()
被调用。
在执行构造器时,执行器会立即执行。异步操作类似于 setTimeout(() => {}, 1000)
// 即使延时设为 0,setTimeout 内部的函数依然需要先加入队列再执行,输出结果为
// 2
// 1
setTimeout(() => console.log(1), 0);
console.log(2);
在执行器内部调用 resolve()
时,会触发异步操作,then()
或 catch()
中的参数根据异步操作结果被调用。
// 输出为:
// promise
// out
// content
let promise = new Promise((resolve, reject) => {
console.log("promise");
resolve();
});
promise.then(() => {
console.log("content");
});
console.log("out");
Promise.resolve()
方法接收一个参数,这个参数将作为已完成状态的 Promise 的结果返回,可以使用 then()
处理。Promise.reject()
方法接收一个参数,这个参数将作为已拒绝状态的 Promise 的结果返回,可以使用 catch()
处理。
将状态为挂起或已完成的 Promise 传入 Promise.resolve()
时返回原 Promise;将状态为已拒绝的 Promise 传入 Promise.reject()
时返回原 Promise;其他情况返回包装原 Promise 的新 Promise。
拥有以 resolve 和 reject 为参数的 then()
方法的对象是非 Promise 的 thenable。将非 Promise 的 thenable 传入 Promise.then()
或 Promise.reject()
时,调用对象的 then()
中执行的方法确定返回哪种状态的 Promise。
let thenable = {
then(resolve, reject) {
resolve(3);
}
};
let p1 = Promise.resolve(thenable); // 状态为已完成的 Promise
p1.then(value => console.log(value)); // 3
let th2 = {
then(resolve, reject) {
reject(7);
}
};
let p2 = Promise.resolve(th2); // 状态为已拒绝的 Promise
p2.catch(err => console.log(err)); // 7
执行器内部抛出错误时,reject()
方法会被调用,捕获这个错误。
全局 Promise 拒绝处理
具有状态为已拒绝且没有得到处理的 Promise 需要有办法来识别。
Node.js 中 process 对象有两个相关的事件。
- unhandledRejection:当状态为已拒绝的 Promise 在事件循环的一个轮次中没有被处理时,该事件会被触发。这个事件函数有两个参数:第一个为拒绝原因,第二个为已拒绝的 Promise。
- rejectionHandled:当状态为已拒绝的 Promise 在事件循环的一个轮次之后被处理时,该事件会被触发。这个事件函数有一个参数,表示已拒绝的 Promise。
浏览器提供的事件触发条件与事件名称与 Node.js 一样。唯一不同的是浏览器提供的事件函数参数为一个对象,该对象有三个属性:事件函数对应的事件类型(type)、已拒绝的 Promise (promise)、拒绝原因(reason)。
串联 Promise
Promise 的 then()
和 catch()
方法被调用完成后会创建并返回一个新的 Promise,根据这个特点可以连续使用 Promise 形成链。
在 Promise 链中,前一个 Promise 在执行器、resolve()
、reject()
中抛出(发生)的错误,可以由紧接着的 Promise 捕获在 reject()
中处理。
在 Promise 链中,前一个 Promise 在 resolve()
、reject()
中返回的值会传入到紧接着的 Promise 的 resolve()
方法中。
在 Promise 链中,前一个 Promise 在 resolve()
、reject()
中返回的 Promise 对象充当了紧接着的 Promise 对象。
响应多个 Promise
Promise.all()
方法接收一个可迭代对象,这个对象的每个元素都是一个 Promise。当每个元素的状态都为已完成时,该方法返回一个 Promise,这个 Promise 的 resolve()
方法参数是一个数组,这个数组的元素为传入的每个 Promise 的 resolve()
参数值,元素顺序与传入 Promise.all()
时的 Promise 顺序一致。或者当传入的 Promise 中如果有一个 Promise 的状态为已拒绝,则该方法返回的 Promise 状态为已拒绝,reject()
方法的参数为状态是已拒绝的 Promise 的 reject()
的参数值。
Promise.race()
方法接收的参数类型与 Promise.all()
一样。区别在于,接收的所有 Promise 中,如果有一个 Promise 的状态变为已完成或已拒绝,则该方法会返回一个新的 Promise,这个 Promise 的状态取决于状态变更到哪种状态,对应状态的方法参数与变更的 Promise 的方法参数值一致。
继承 Promise
将 Promise 作为基类通过继承定义自定义 Promise 可以扩展 Promise 的功能。自定义 Promise 继承了静态方法 resolve()
、reject()
、race()
、all()
。后两个方法行为与 Promise 一致;前两个方法返回自定义 Promise 对象,由 [[Symbol.species]] 属性决定返回类型。
异步任务运行
function run(generator) {
let iterator = generator();
let result = iterator.next();
(function step(){
if (!result.done) {
let p = Promise.resolve(result.value);
p.then(value => {
result = iterator.next(value);
step();
}).catch(error => {
result = iterator.throw(error);
step();
});
}
}());
}
function asy(name) {
return new Promise((resolve, reject) => {
read(name, (result, err) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
}
参考
[1] Zakas, Understanding ECMAScript 6, 2017.