浏览器的事件循环机制与原理
2024-09 1
单队列事件循环
- 引入消息队列:通过引入一个队列的结构存储消息,IO 线程中产生的新任务添加进消息队列尾部。
- 循环读取对头:渲染主线程会循环地从消息队列头部中读取任务,执行任务。
- 退出机制:页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如果设置了,那么就直接中断当前的所有任务,退出线程。
消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。鉴于这个属性,就有如下两个问题需要解决。
1、无法处理高优先级的任务:
对于高优先级的任务,会有执行效率和实时性降低的问题。
- 如监听DOM节点变化情况,若是采用观察者模式监听变化,当变化发生时,渲染引擎同步调用这些接口,则会因为DOM变化非常频繁,拉长了当前的任务执行时间,从而导致执行效率的下降。
- 若是采用同步通知的方式,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了,这就会影响到监控的实时性。
针对这种情况,可以引入微任务队列解决。
2、无法解决单个任务执行时长过久的问题。
由于是队列的方式,且是单线程处理,故一次只能处理一个任务,当某个任务执行过久时,会导致往后的任务需要等待很长时间。如JS执行过久占用动画帧的时间,就会造成卡顿。
针对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。
多队列事件循环
- 引入微任务队列,解决高优先级问题:消息队列中的任务称为宏任务,每个宏任务都包含一个微任务队列,如DOM任务变化就是添加到微任务队列中,每个宏任务执行完后,执行当前宏任务的微任务队列。
常见的宏任务有:
- script脚本和DOM操作:脚本中可能会修改DOM,对 DOM 结构进行增删改操作,比如添加节点、移除节点等。
- setTimeout 和 setInterval:用于设置定时任务,在指定的时间间隔后执行任务。
- XHR 和 Fetch 请求:发送网络请求并接收响应数据。
- 事件处理器:例如点击事件、键盘事件等。
- I/O 操作:例如文件读写、数据库操作等。
常见的微任务有:
- Promise.then 和 Promise.catch:Promise 对象的回调函数会被添加到微任务队列中,在 Promise 对象状态改变后执行。
- Async/Await:await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
- MutationObserver:用于监听 DOM 变化的 API,当 DOM 发生变化时,会触发微任务队列。
- process.nextTick:Node.js 中的微任务 API,用于将回调函数添加到微任务队列中
- 引入延时队列,解决单个宏任务执行过久的问题: 每次执行完一个宏任务后,会清空微任务队列,之后再遍历延时队列,根据开始时间和延时时间,计算出到期的任务,然后依次执行到期的任务,另外,
setTimeout
的回调函数会被在延时队列(其实是hashmap
结构)中。
补充:setTimeOut的执行机制
-
如果当前任务执行时间过久,会影响定时器任务的执行。
-
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。 在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。
-
未激活的页面,setTimeout 执行最小间隔是 1000 毫秒。 如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。
-
延时执行时间有最大值。 Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,溢出会被重置为0。
-
使用 setTimeout 设置的回调函数中的 this 丢失。如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象
var name= 1; var MyObj = { name: 2, showName: function(){ console.log(this.name); } } setTimeout(MyObj.showName,1000) // 非严格模式输出1、严格模式输出undefined
上述代码非严格模式下输出1,这是因为通过
MyObj.showName
传递给setTimeout
第一个参数时,实际上是传递了一个函数引用,这种情况下,this
的上下文丢失了。解决方法也很简单,那就是不要通过引用传参。
-
因此可以包一层箭头函数或者匿名函数,在函数内部执行。
//箭头函数 setTimeout(() => { MyObj.showName() }, 1000); //或者function函数 setTimeout(function() { MyObj.showName(); }, 1000)
-
或者使用bind方法:
setTimeout(MyObj.showName.bind(MyObj), 1000)
-
- 无目录