Javascript事件循环如何在事件出列后处理非阻塞函数调用的执行



假设调用堆栈上有5个内容和事件队列中的一个项目。一旦所有5个项目都从调用堆栈中弹出,事件队列中的回调就会被推送到调用堆栈中(这可能需要20秒才能完成)。同时,我向调用堆栈添加了另一个(非阻塞)调用。如果I/O密集型操作仍在执行,这将如何工作?系统是否暂时冻结?

正如您所说,它是一个循环,或者正如JavaScript规范所说,是一个作业队列。脚本顶层的初始执行是一项作业;事件处理程序回调是一项作业;计时器回调是一项作业;等

从作业队列中提取作业时,该作业将一直运行到完成。如果需要20秒,则需要20秒。处理该作业队列的线程在这20秒内不能做任何其他事情。如果你在网络浏览器的主UI线程上这样做,它会在很大程度上冻结浏览器的UI。(当然,如果你在工作线程上执行,它只会阻塞工作线程。)

我问您在调用堆栈中添加(非阻塞)调用是什么意思。你说:

假设您执行一些操作,如单击按钮,将另一个调用添加到调用堆栈中。

单击具有事件处理程序的按钮不会向调用堆栈添加调用;它将一个作业添加到作业队列中。(作业执行代码中的函数调用foo();会向调用堆栈添加一个调用

我应该注意到有两种标准的工作:脚本工作和承诺工作。(或者HTML规范称之为任务和微任务。)主脚本执行、DOM事件回调和计时器回调都是脚本作业/任务(也称为"宏任务")。承诺反应(调用承诺的实现或拒绝处理程序)是承诺作业/微任务。不同的是,当脚本作业(任务)正在运行时,它调度的任何promise作业(微任务)都将在该作业结束时运行,而不是添加到主作业队列中。由promise作业调度的任何promise作业都将在脚本作业处理的同一个结束期间运行。也就是说,promise作业/微任务比脚本作业/任务具有更高的优先级。

你可以在这里看到这种情况:

// This script is running in a script job / task
// Unsurprisingly, this is the first thing you see in the console
console.log("Main script job begin");
// Here, we schedule a script job / task for an immediate timer callback:
setTimeout(() => {
console.log("Timer job");
}, 0);
// After doing that, we schedule a Promise fulfillment callback:
Promise.resolve().then(() => {
console.log("Promise fulfillment job 1 begin");
Promise.resolve().then(() => {
console.log("Promise fulfillment job 2");
});
console.log("Promise fulfillment job 1 end");
});
// For emphasis, we'll output something before either happens;
// this is the second thing you see in the console.
console.log("Main script job end");

输出为:

主脚本作业开始主脚本作业结束承诺履行工作1开始承诺履行工作1结束承诺履行工作2计时器作业

当浏览器加载脚本时,它会将脚本作业队列中的作业排队以运行该脚本。JavaScript线程在下一次执行循环时会拾取该作业,然后发生这种情况:

  1. 输出第一条消息,表示主作业已开始
  2. 调用setTimeout,在约0ms内调度计时器回调。这会将计时器添加到主机的计时器列表中,计划执行时间为0ms。由于延迟为0ms,主机可能会也可能不会立即将脚本作业添加到脚本作业队列中以调用计时器回调(也可能会等到稍晚才执行)
  3. 调用Promise.resolve,创建一个用值undefined实现的承诺
  4. then被要求履行已达成的承诺。由于promise已结算,因此会立即向当前脚本作业的promise作业队列添加一个作业来调用履行回调
  5. 输出脚本末尾的第二条消息,显示主脚本作业正在结束
  6. 由于脚本作业已到达末尾,因此将处理其promise作业队列。其中有一个条目(用于调用我们的第一个履行处理程序),因此调用该函数
  7. 该功能:
    1. 输出"Promise implementation job 1 begin"消息
    2. 创造另一个实现的承诺
    3. 调用then为其添加一个履行处理程序。
      1. 由于promise已经结算,因此会立即向promise作业队列中添加一个作业,以调用第二个履行处理程序
    4. 输出其"Promise implementation job 1 end"消息
  8. 由于promise作业已经完成,JavaScript引擎会查看promise作业队列,看看是否还有剩余的条目。有一个,所以它调用条目指定的函数
  9. 该函数输出其"Promise implementation job 2"消息
  10. 由于promise作业已经完成,JavaScript引擎会查看promise作业队列,看看是否还有剩余的条目。没有,所以脚本作业循环继续
  11. 此时,调用计时器回调的作业可能在脚本作业队列中。如果没有,主机环境可能会在此时添加它,或者让事件循环几次。不过,最终,它肯定会将作业放入脚本作业队列中,以调用计时器回调
  12. JavaScript引擎获取该作业,调用计时器回调,回调输出其消息

因此,即使在步骤4中第一个承诺履行处理程序被放入承诺作业队列之前,定时器回调可能在步骤2中被添加到脚本作业队列中,但该履行处理程序会首先运行。由于该promise作业将另一个promise作业排入队列(步骤7.3.1),因此第二个promise job也将首先运行


我说两种"标准"类型的作业/任务,因为环境提供了其他东西(如Node.js的setImmediate或浏览器的requestAnimationFrame),它们与两种主要的作业类型/队列类型有些不同。


线程可以通过Atomics.wait暂停,但不能用于处理队列中的另一个作业。大多数JavaScript引擎不允许暂停主线程(浏览器中的UI线程,Node.js中的主线程),但允许暂停工作线程。

最新更新