为什么 Javascript 规范设计者在 promise 中引入了 microtask
Why Javascript specification designer introduced microtask in promise
我对microtask和Promise有所了解,比如Promise.prototype.then
中的handler会被入队microtask,一旦调用栈为空就会执行,它可以用来避免race condition或者其他关于并发的东西.
但是如果我们不引入 microtask 会怎样呢?
让我们来看这个示例代码:
Promise.resolve("resolved").then(i => console.log("resolved"));
console.log("start");
这是一个立即(同步)解决的承诺。当执行 then
方法时,它已经处于已解决状态。然而,对 then
的回调不会立即执行。相反,一个作业被放入作业队列(微任务)。所以我们得到这个输出:
start
resolved
你的问题是如果没有这样的作业调度它是否可以工作,这意味着回调将立即执行以便结果将是:
resolved
start
对于示例代码,这确实是一个可行的替代方案。但是现在再举一个例子,promise 不是由 JavaScript 代码解决的,而是由一个较低级别的事件解决的,该事件深深地埋在一个 API 中,该事件用一个 promise 接口封装了它。假设它是一些 API 来读取文件内容:
readFile(path).then((content) => console.log("file contents: " + content));
console.log("start of a lot of work");
for (let i = 0; i < 1e7; i++) {
// some work
}
console.Log("end of a lot of work");
因此,根据 Promise 规范,这将输出:
start of a lot of work
end of a lot of work
file contents: .....
现在假设工作循环需要 3 秒才能完成,并且文件读取 API 有一个将大部分工作委托给操作系统函数的实现。那些 OS 函数将 运行 符合 OS 处理架构,这通常是某种形式的事件驱动多任务处理。这里的本质是 OS 将能够在 JS 循环进行的同时进行文件操作。
我们也假设文件读取在1秒内完成。事件从 OS 冒泡到 API 的非 JavaScript 中间件,准备冒泡到 JavaScript "world"。现在应该怎么办?我们可以想象的有两种选择:
一个作业被放入作业队列,这表明承诺已解决并且需要调用 then
回调。这就是它的实际工作方式。
运行ning JavaScript 循环被中断,readFile
返回的承诺解决,[= =15=] 在那一刻调用回调。回调完成后,被中断的执行上下文将恢复,并继续完成。
后一种机制可能会导致整个中断链,其中甚至 then
回调中的代码也可能被另一个 promise-resolution 事件中断,...等等。这可能导致必须像堆栈一样维护的执行上下文的构建。这会使代码执行变得不可预测、难以调试并且容易发生堆栈溢出。
在我看来,这些就是为什么这个选项不受欢迎的原因,也是为什么我们有选项 1 的原因。即使第二个选项适用于纯粹使用 JavaScript 创建的承诺——不依赖于一些较低级别的异步行为——如果这些承诺与由提供的承诺具有不同的行为,那将是奇怪的API秒。在 then
回调执行和作业队列的使用方面,它们都应该使用相同的行为。
我对microtask和Promise有所了解,比如Promise.prototype.then
中的handler会被入队microtask,一旦调用栈为空就会执行,它可以用来避免race condition或者其他关于并发的东西.
但是如果我们不引入 microtask 会怎样呢?
让我们来看这个示例代码:
Promise.resolve("resolved").then(i => console.log("resolved"));
console.log("start");
这是一个立即(同步)解决的承诺。当执行 then
方法时,它已经处于已解决状态。然而,对 then
的回调不会立即执行。相反,一个作业被放入作业队列(微任务)。所以我们得到这个输出:
start
resolved
你的问题是如果没有这样的作业调度它是否可以工作,这意味着回调将立即执行以便结果将是:
resolved
start
对于示例代码,这确实是一个可行的替代方案。但是现在再举一个例子,promise 不是由 JavaScript 代码解决的,而是由一个较低级别的事件解决的,该事件深深地埋在一个 API 中,该事件用一个 promise 接口封装了它。假设它是一些 API 来读取文件内容:
readFile(path).then((content) => console.log("file contents: " + content));
console.log("start of a lot of work");
for (let i = 0; i < 1e7; i++) {
// some work
}
console.Log("end of a lot of work");
因此,根据 Promise 规范,这将输出:
start of a lot of work
end of a lot of work
file contents: .....
现在假设工作循环需要 3 秒才能完成,并且文件读取 API 有一个将大部分工作委托给操作系统函数的实现。那些 OS 函数将 运行 符合 OS 处理架构,这通常是某种形式的事件驱动多任务处理。这里的本质是 OS 将能够在 JS 循环进行的同时进行文件操作。
我们也假设文件读取在1秒内完成。事件从 OS 冒泡到 API 的非 JavaScript 中间件,准备冒泡到 JavaScript "world"。现在应该怎么办?我们可以想象的有两种选择:
一个作业被放入作业队列,这表明承诺已解决并且需要调用
then
回调。这就是它的实际工作方式。运行ning JavaScript 循环被中断,
readFile
返回的承诺解决,[= =15=] 在那一刻调用回调。回调完成后,被中断的执行上下文将恢复,并继续完成。
后一种机制可能会导致整个中断链,其中甚至 then
回调中的代码也可能被另一个 promise-resolution 事件中断,...等等。这可能导致必须像堆栈一样维护的执行上下文的构建。这会使代码执行变得不可预测、难以调试并且容易发生堆栈溢出。
在我看来,这些就是为什么这个选项不受欢迎的原因,也是为什么我们有选项 1 的原因。即使第二个选项适用于纯粹使用 JavaScript 创建的承诺——不依赖于一些较低级别的异步行为——如果这些承诺与由提供的承诺具有不同的行为,那将是奇怪的API秒。在 then
回调执行和作业队列的使用方面,它们都应该使用相同的行为。