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

2023-12-28

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


正如你所说,这是一个循环,或者正如 JavaScript 规范所说,一个作业队列 https://tc39.github.io/ecma262/#sec-jobs-and-job-queues。脚本顶层的初始执行是一个作业;事件处理回调是一项工作;计时器回调是一项工作; ETC。

当从作业队列中选取一个作业时,它会运行直至完成。如果这需要 20 秒,那么它就需要 20 秒。处理作业队列的线程在这 20 秒内不能执行任何其他操作。如果您在 Web 浏览器的主 UI 线程上执行此操作,则会很大程度上冻结浏览器的 UI。 (当然,如果您在工作线程上执行此操作,它只会阻塞工作线程。)

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

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

单击具有事件处理程序的按钮不会将调用添加到调用堆栈;它将作业添加到作业队列中。 (函数调用,foo();,在作业的执行代码中添加对调用堆栈的调用。)如果线程正忙于处理另一个作业,则该作业会坐在那里等待线程完成当前正在处理的任何作业时被拾取。

我应该指出,有两种标准的作业:脚本作业和承诺作业。 (或者如 HTML 规范所称,任务和微任务。)主脚本执行、DOM 事件回调和计时器回调都是脚本作业/任务(也称为“宏任务”)。 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");

输出是:



Main script job begin
Main script job end
Promise fulfillment job 1 begin
Promise fulfillment job 1 end
Promise fulfillment job 2
Timer job
  

当浏览器加载脚本时,它会将作业放入脚本作业队列中以运行该脚本。 JavaScript 线程在下次执行循环时会继续执行该作业,并且会发生这种情况:

  1. 输出的第一条消息表明主要作业已开始。
  2. setTimeout被调用,在 ~0ms 内安排一个计时器回调。这会将计时器添加到主机的计时器列表中,并在未来安排约 0 毫秒的执行时间。由于延迟为 0 毫秒,因此主机可能会也可能不会立即将脚本作业添加到脚本作业队列中以调用计时器回调(或者它可能会等到稍后才执行此操作)。
  3. Promise.resolve被称为,创造一个用价值实现的承诺undefined.
  4. then是对那既定的应许的召唤。由于 Promise 已解决,因此会立即添加一个作业来调用履行回调到当前脚本作业的 Promise 作业队列。
  5. 输出脚本末尾的第二条消息,表明主脚本作业正在结束。
  6. 由于脚本作业已到达末尾,因此其 Promise 作业队列已被处理。其中有一个条目(调用我们的第一个履行处理程序),以便调用该函数。
  7. That function:
    1. 输出其“承诺履行作业 1 开始”消息。
    2. 创造另一个兑现的承诺。
    3. Calls then to add a fulfillment handler to it.
      1. 由于承诺已经解决,因此会立即将作业添加到承诺作业队列中以调用第二个履行处理程序。
    4. 输出“承诺履行作业 1 结束”消息。
  8. 由于该 Promise 作业完成,JavaScript 引擎会查看 Promise 作业队列以查看是否有任何剩余条目。有一个,所以它调用entry指定的函数。
  9. 该函数输出其“Promisefulfillmentjob2”消息。
  10. 由于该 Promise 作业完成,JavaScript 引擎会查看 Promise 作业队列以查看是否有任何剩余条目。没有,因此脚本作业循环继续。
  11. 此时,调用计时器回调的作业可能位于脚本作业队列中。如果不是,主机环境可能会在此时添加它,或者可能让事件循环循环几次。但最终,它肯定会将作业放入脚本作业队列中以调用计时器回调。
  12. JavaScript 引擎接收该作业,调用计时器回调,然后回调输出其消息。

因此,即使在第 4 步将第一个 Promise 履行处理程序放入 Promise 作业队列之前,计时器回调可能已在第 2 步中添加到脚本作业队列,但该履行处理程序会首先运行。并且因为该 Promise 作业将另一个 Promise 作业排队(步骤 7.3.1),所以第二个 Promise 作业also首先运行。


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


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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

事件出队后,Javascript 事件循环如何处理非阻塞函数调用的执行? 的相关文章

随机推荐