哪个任务(setTimeout 或点击事件)在任务队列中优先?

Which task(setTimeout or click event) is prioritized in the task queue?

我正在学习执行栈、任务队列和事件循环机制。

我连续点击按钮直到主线程可用(就在函数 a() 完成之前),如下所示。
我以为click(UI)事件和setTimeout使用同一个队列,叫Macrotask或Task Queue,所以当我在1s和3s之间点击按钮的时候,我以为task1和task2之间打印了task2的log。但结果并非如此。 Task2(点击事件)总是先打印,setTimeout事件(task1,task3)在点击事件之后打印。

所以我想知道点击事件是否使用与 setTimeout 不同的队列机制,或者点击事件优先于 setTimeout。

提前感谢您的帮助

操作

  1. 单击按钮(任务 2)
  2. --------1000ms setTimeout task1--------
  3. 单击按钮(任务 2)
  4. --------3000ms setTimeout task3--------
  5. 单击按钮(任务 2)
  6. --------6000ms 主线程现在可用--------

我的期望日志顺序

fn a done
task2 (click) done
task1 (setTimeout 1000ms) done
task2 (click) done
task3 (setTimeout 3000ms) done
task2 (click) done

结果日志顺序

fn a done
task2 (click) done
task2 (click) done
task2 (click) done
task1 (setTimeout 1000ms) done
task3 (setTimeout 3000ms) done

代码

const btn = document.querySelector('button');
btn.addEventListener('click', function task2() {
  console.log('task2 (click) done');
});

function a() {
  setTimeout(function task1() { 
    console.log('task1 (setTimeout 1000ms) done');
  }, 1000);

  setTimeout(function task3() { 
    console.log('task3 (setTimeout 3000ms) done');
  }, 3000);

  // hold main thread for 6000ms(*1)
  const startTime = new Date();
  while (new Date() - startTime < 6000);

  console.log('fn a done');
}

a();
<button>button</button>
<script src="main.js"></script>

让我们来看看你用 setTimeout 和 while 循环做了什么。

如果您 运行 代码片段,您会看到 1 秒和 3 秒超时的时间戳基本相同。这是因为 while 循环阻塞主线程 6 秒,直到它可用于 运行 setTimeout 中的函数。

回调不会恰好在 1 秒或 3 秒后执行,而是仅在主线程空闲时执行。 setTimeout 保证回调函数在最少 x 毫秒后执行,对于任务 1 >= 1 秒,对于任务 3 >= 3 秒。

function a() {
  setTimeout(function task1() { 
    console.log('task1 (setTimeout 1000ms) done ' + new Date());
  }, 1000);

  setTimeout(function task3() { 
    console.log('task3 (setTimeout 3000ms) done ' + new Date());
  }, 3000);

  // hold main thread for 6000ms(*1)
  const startTime = new Date();
  while (new Date() - startTime < 6000);

  console.log('fn a done');
}

a();

I thought click(UI) events and setTimeout uses the same queue

他们没有。

UI 事件在大多数浏览器中使用 user interaction(UI) task source, which in most browsers has its own task-queue, setTimeout uses the timer task-source which also has its own task-queue

虽然规范没有要求,但 UI 任务源在几乎所有浏览器中的所有任务源中具有最高优先级之一,而计时器具有最低优先级之一。

这种优先级排序的工作原理是,在 event-loop's processing model 的第一步,用户代理 (UA) 必须选择其事件队列之一,任务 它将执行。

注意: 通俗地称为 “宏任务” 的是任何不是 microtask 的任务。
The microtask queue is not a task queue and while a microtask can be chosen as the main task in the first step of the event loop processing, the microtask queue can't be prioritized because it has to be emptied synchronously at each microtask-checkpoints, which can happen several times during each event-loop iteration, and particularly after the chosen task gets executed.

所以在这里,当 while 循环完成阻塞事件循环,并且下一次迭代开始时,UA 将不得不选择它应该从哪个任务队列中选择下一个任务。 它将在其 UI 任务队列 中看到有新事件在等待,并执行它们,因为它们具有更高的优先级。
然后当它们都执行完后,它会选择定时器队列并按照它们预定的时间顺序执行它们。

另请注意,they have 饥饿系统可防止高优先级任务队列长时间阻塞其他任务队列。


最后,我要提一下,有一项提议让我们的 Web 开发人员直接处理所有这些优先事项:main thread scheduling

使用此实验性功能,我们可以将代码段重写为

if( !("scheduler" in window) ) {
  console.error("Your browser doesn't support the postTask API");
  console.error("Try enabling the Experimental Web Platform features in chrome://flags");

}
else {
  scheduler.postTask(() => { 
    console.log('task1 (background) done');
  }, { priority: "background" } );

  scheduler.postTask(() => { 
    console.log('task2 (background) done');
  }, { priority: "background" } );

  // hold main thread for 6000ms(*1)
  const startTime = new Date();
  while (new Date() - startTime < 2000);
  scheduler.postTask(() => { 
    console.log('task3 (user-blocking) done');
  }, { priority: "user-blocking" } );

  console.log('synchronous done');
}

并看到最终任务先执行。