浏览器的事件循环机制与原理

2024-09 1

单队列事件循环

  • 引入消息队列:通过引入一个队列的结构存储消息,IO 线程中产生的新任务添加进消息队列尾部。
  • 循环读取对头:渲染主线程会循环地从消息队列头部中读取任务,执行任务。
  • 退出机制:页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如果设置了,那么就直接中断当前的所有任务,退出线程。

e2582e980632fd2df5043f81a11461c6.png

消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。鉴于这个属性,就有如下两个问题需要解决。

1、无法处理高优先级的任务:

对于高优先级的任务,会有执行效率和实时性降低的问题。

  • 如监听DOM节点变化情况,若是采用观察者模式监听变化,当变化发生时,渲染引擎同步调用这些接口,则会因为DOM变化非常频繁,拉长了当前的任务执行时间,从而导致执行效率的下降
  • 若是采用同步通知的方式,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了,这就会影响到监控的实时性

针对这种情况,可以引入微任务队列解决。

2、无法解决单个任务执行时长过久的问题。

由于是队列的方式,且是单线程处理,故一次只能处理一个任务,当某个任务执行过久时,会导致往后的任务需要等待很长时间。如JS执行过久占用动画帧的时间,就会造成卡顿。

8de4b43fca99b180fdffe6a5af07b5cc.png

针对这种情况,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的执行机制

  1. 如果当前任务执行时间过久,会影响定时器任务的执行。

  2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。 在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒。 如果标签不是当前的激活标签,那么定时器最小的时间间隔是 1000 毫秒,目的是为了优化后台页面的加载损耗以及降低耗电量。

  4. 延时执行时间有最大值。 Chrome、Safari、Firefox 都是以 32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,溢出会被重置为0。

  5. 使用 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)