聊聊浏览器宏任务的优先级

lxf2023-05-05 18:05:01

这篇文章源于我之前写过的一篇关于MessageChannel的文章,里面举到一个例子

setTimeout(() => {
    console.log('setTimeout')
}, 0);

const { port1, port2 } = new MessageChannel();
port2.onmessage = function () {
    console.log('MessageChannel');
};
port1.postMessage('ping');

Promise.resolve().then(() => {
    console.log('Promise');
});

当时我说上面这段代码中MessageChannel先于setTimeout打印。因为我查过资料,了解到宏任务是有优先级的,DOM事件的优先级就比timer的优先级高,而MessageChannel的message事件又属于DOM事件,所以我得出先打印MessageChannel再打印setTimeout的结论。而且印象中我也在chrome中验证过这个结论。

但文章的评论区很多同学反映他们执行的结果是先打印setTimeout再打印MessageChannel,我试过之后发现的确如此。

stackoverflow上有关于宏任务优先级的问题,大家可以看下Kaiido的回答(1和2),他也认为MessageChannel比setTimeout有更高的优先级。但实际上以当前chrome的运行结果来看,两者有相同的优先级,输出顺序取决于它们在代码里的先后顺序。

为什么出现这样的问题,这篇文章就来研究一下。

本文大部分代码都是基于当前最新的chrome测试的,版本号为110.0.5481.100。

排查

在90.0.4430.212版本的chrome上执行上面的代码,MessageChannel的确在setTimeout之前打印。而从MessageChannel那篇文章发表的时间来看,当时使用的chrome版本应该介于90.0.4430.212和110.0.5481.100之间,表现跟90.0.4430.212版本一致。

所以,可以确定的是,chrome在90.0.4430.212和110.0.5481.100之间的某个版本修改了MessageChannel和setTimeout的优先级顺序。

至于具体是哪个版本做出更改,以及为什么更改,由于时间精力有限,我并没有找出确切答案。

为什么宏任务的优先级可以随意变更?规范失效了吗?下面就来回答这个问题。

事件循环

网上关于事件循环的文章很多,这里简单回顾一下事件循环的流程: 聊聊浏览器宏任务的优先级

一次事件循环的过程:

  1. 取出一个宏任务执行;
  2. 执行所有微任务直至清空微任务队列;
  3. 如果需要渲染,执行渲染更新。

事件循环就是不断重复上面3个步骤。更多的说法是:

  1. 执行同步代码(也属于宏任务),只执行一次,过程中遇到异步代码交给不同的web api异步处理,处理完之后会将回调对应的任务推入宏任务队列;
  2. 执行并清空所有微任务;
  3. 执行下一个宏任务。

网上用得比较多的一张图是: 聊聊浏览器宏任务的优先级

但我觉得下面这张更精确,因为它表明了宏任务队列可能有多个。

聊聊浏览器宏任务的优先级

HTML标准里是这样描述的:

An event loop has one or more task queues. A task queue is a set of tasks. Task queues are sets, not queues, because the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.

这里的task queue指的就是宏任务队列。事件循环有一个或多个宏任务队列,每个宏任务队列是任务的一个集合(注意,实际是集合而不是队列)。因为宏任务队列中保存的任务并不一定都是可执行的,事件循环处理模型每次取的都是可执行的第一个任务,行为上并不是队列的FIFO。

一般来说,任务都是可执行的,所以我们可以忽略单个宏任务队列中任务的优先级,重点关注不同的宏任务队列之间的优先级。

影响宏任务优先级的因素

浏览器的实现

我们知道,宏任务包括执行整体的js代码、DOM事件回调、XHR回调、计时器、IO操作和UI render。

标准描述了4种通用的宏任务来源:

  1. DOM manipulation任务源:DOM操作相关的,非阻塞性的,比如往document中插入元素。对应Prioritized Task Scheduling API的user-visible等级;
  2. user interaction任务源:用户交互相关的,比如键盘或鼠标输入,最常见的就是click事件。对应Prioritized Task Scheduling API的user-blocking等级;
  3. networking任务源:网络活动相关;
  4. navigation and traversal任务源:导航和history寻访相关。

另外,在浏览器的实现中,应该还有timer任务源等。

不同任务源的任务会放到不同的队列。

HTML标准虽然定义了任务源,但没有定义任务源的优先级,优先级由各个浏览器厂商自己决定。因此,并不是规范失效,而是规范给了浏览器厂商自由度,不同浏览器的宏任务队列的顺序可能不一样。但比较一致的是,在大多浏览器中,user interaction相关的宏任务拥有最高的优先级,timer相关的宏任务优先级则较低。

防饥渴机制

对于任务的调度,浏览器有防饥渴机制,避免高优先级的任务队列一直执行,导致低优先级的任务队列永远得不到执行的机会。

这也有可能给宏任务的执行顺序带来不确定性。

测试

结合Kaiido提供的代码和Prioritized Task Scheduling API,在当前chrome上可以得出以下优先级顺序: user-blocking = user interaction > user-visible = DOM manipulation = timer = MessageChannel = naviation and traversal > networking > background

MessageChannel和timer有相同的优先级,代码书写顺序决定它们的打印顺序。

总结

  1. 宏任务队列可能有多个,而且严格来说它们是集合,不同的队列优先级不同;
  2. 浏览器实现的自由度以及防饥渴机制也有可能带来宏任务优先级的不稳定;
  3. 当前chrome版本测得,MessageChannel和timer具有相同的优先级。

参考资料

  1. stackoverflow.com/a/66978351/…
  2. stackoverflow.com/a/70913524
  3. html.spec.whatwg.org/multipage/w…
  4. github.com/WICG/schedu…
  5. lynnelv.github.io/js-event-lo…
  6. yeefun.github.io/event-loop-…
  7. wicg.github.io/scheduling-…