剖析浏览器的事件调度

446 阅读4分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

事件循环

首先是浏览器页面的渲染进程的主线程要通常执行很多任务,比如渲染DOM节点、JavaScript任务执行以及等,这些任务是由于用户不断的与页面产生交互而产生,如果不能有序地进行安排,那么页面的展示效果可能与用户所期望的效果就会产生出入,因此需要一个事件的调度系统来管理这些任务的执行。在这之中浏览器有一个渲染页面元素的主线程作为处理页面重绘,回流等设计元素样式操作的进程。同时还会有一个任务队列来管理javaScript中获取到的任务,比如按键的点击操作,窗口的scroll操作,与远程服务器通信的操作等等。这也就是js的执行上下文的EventLoop

EventLoop的任务分类

jsEventLoop中通常有两种任务即宏任务与微任务。微任务存储在微任务队列micro task queue中,用来存储那些需要优先处理的任务,比如Promise任务以及Mutation Observer任务都会放入微任务队列中优先执行。而宏任务如SetTimeoutsetInterval等产生时会放入宏任务队列等待执行,但在每个宏任务执行之前,会对当前的微任务队列进行检测,如果存在微任务,则优先清空微任务队列中的微任务,再执行宏任务。

setTimeout(()=>{
    console.log(1)
})
Promise.resolve(2).then((v)=>{
    console.log(v)
})
setTimeout(Promise.resolve(3).then(v=>console.log(v)))
//2
//3
//1

但是v8对于宏任务的调度策略不能满足对时间精度要求较高的需求,由于

  • JavaScript无法准确地知道某个任务在消息队列中的位置,因而无法准确地控制任务的执行时间。
  • JavaScript添加的任务之间可能会有其他的系统安排的任务添加到消息队列。

XMLHttpRequest

XMLHttpRequest是比较特殊的一个Api,使用方法是:

let xhr = new XMLHttpRequest()
xhr.xxxx()
...

这也就是常说的Ajax请求的核心,这个任务在浏览器的调度过程中是算作宏任务来处理,试想如果一个网络请求很慢,那么这个请求作为宏任务的回调就会异步执行,然后等待完成。

requestAnimationIframe

requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

requestAnimationFrame是一个比较特殊的api,它是在某些场景渲染我们的需要动画时用到的api,那么它是怎么被浏览器调度的呢,通常我们的处理之中,浏览器会在保持任务顺序的前提下,大约分配四分之三的优先权给鼠标和键盘这类回调事件,保证用户的输入得到最高优先级的响应,而剩下的优先级交给其他任务Task,并且保证让他们得到执行。那么既然需要在下次重绘之前执行requestAnimationFrame的任务,那么是不是按微任务的优先级执行呢,答案是:不会。因为如果将它作为一个微任务执行,那么也需要会在一个过高的优先级执行以至于可能会丢失一些异步操作以至于UI更新的不连贯,而它也并不是严格的以宏任务的顺序执行,它会严格的根据浏览器的帧率在每次重绘之前执行。

requestIdleCallback

window.requestIdleCallback() 方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

这个api目前还是实验性功能,但是提供了react新的任务调度的思路,它的出现时为了让浏览器流畅的执行渲染操作,让控制权在浏览器和用户操作之间变化,保证响应用户操作的同时不会阻塞其他渲染任务的执行。

参考: