浏览器事件循环EventLoop

77 阅读6分钟

一、进程和线程的关系

程序运行都有它自己专属的内存空间,我们可以把这块空间简单理解为进程。

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

有了进程之后,运行代码的【人】称之为【线程】,一个进程至少有一个线程,所以在进程开启后,会自动创建一个线程来运行代码,该线程称之为主线程。

即进程和线程可以简单理解为

  • 火车(进程)和车厢(线程)
  • 一辆火车可以有多个车厢,即一个进程可以有多个线程
  • 乘客可以在同一辆火车之内随意走动(数据共享)
  • 不同火车之间要转乘需要有凭证(需要另一个进程的同意)
  • 不同火车之间相互独立,即不同的进程之间相互不影响
  • 同一辆火车的某个车厢要是出事了,会导致整辆火车无法发车,即同个进程内的线程出问题,会导致进程出问题

二、浏览器的进程

浏览器的进程主要有浏览器进程、渲染进程、网络进程等,其中我们主要关注渲染进程。

渲染进程(目前谷歌浏览器为每个标签页一个渲染进程,将来可能会变,详见谷歌官方文档)又包括GUI渲染线程、js引擎线程、事件触发器线程、定时器触发线程、异步http请求线程

image.png

三、事件循环Event Loop(消息循环)

3.1 同步任务和异步任务

  • 同步任务
    • 按代码书写顺序,从上到下,从左到右依次执行代码
  • 异步任务
    • 异步任务指得是js代码执行过程中无法立即处理的任务(会导致js执行阻塞)
    • 异步任务又分为宏任务和微任务
    • 宏任务
      • setTimetout、setInterval定时器
      • 网络请求
      • 交互事件
      • I/O事件
      • script整体代码
    • 微任务
      • promise
      • mutationObserver
      • process.nextTick

3.2 执行栈和任务队列

  • 执行栈
    • 在js代码运行的过程中,会创建一个执行栈,这个栈采用先进后出原则,然后创建一个全局上下文,并将这个作用域压入栈中,之后依次将js代码进行压栈和出栈操作
    • 在js代码执行过程中,若遇到函数的调用时,会创建一个新的函数上下文,然后进行压栈操作,当这个函数执行完毕后,会进行出栈操作

image.png

  • 任务队列
    • 任务队列又分为宏任务、微任务
    • 在js代码运行的过程中(渲染主进程开启后),会创建一个渲染主线程,负责处理页面渲染和用户界面的交互,它主要任务为
      • HTML 解析和 DOM 构建: 渲染主线程负责解析HTML文档并构建DOM(文档对象模型)树。
      • CSS 解析和样式计算: 渲染主线程解析CSS并计算元素的样式。
      • 布局(Layout): 渲染主线程确定页面上元素的位置和大小。
      • 绘制(Painting): 渲染主线程将页面上的元素绘制到屏幕上。
      • 处理用户交互: 渲染主线程响应用户的交互事件,如鼠标点击、滚动、键盘输入等。
    • 运行时,首先会将整体的script代码(任务)放到渲染主线程中(执行栈)进行执行
    • 当遇到宏任务或微任务等异步任务时,
      • 如当遇到定时器时,会调用定时器触发线程进行计时,然后渲染线程会跳过定时器任务,继续往后执行代码
      • 当定时器计时时间到了之后,定时器触发线程会将js代码回调包装成一个任务,加入到宏任务队列的尾部
    • 当执行栈中的代码执行完毕后,渲染主线程会排查任务队列
      • 即先查看微任务队列中是否有任务,若有,则将微任务放到执行栈中执行
      • 当所有的微任务执行完毕后,再排查是否有宏任务,若有,则将宏任务放到执行栈中执行
      • 在执行异步代码的时候,若又遇到异步任务,则会继续执行上面的步骤
    • 当所有任务都执行完毕后,渲染主线程会进入休眠状态
    • 其他线程随时可以向任务队列中注入任务,当有任务后,又会触发渲染主线程执行

image.png

也就是说一次EventLoop会执行一个宏任务和当前任务产生的微任务

image.png

image.png

image.png

四、如何理解js的异步

js是一门单线程语言,这主要是因为js运行在渲染主线程上,而渲染主线程只有一个,渲染主线程承担着页面渲染、js执行等。
若采用同步的方法,则很大程度会导致js代码阻塞,导致消息队列中很多其他任务无法得到执行,这样一来会,一方面会导致繁忙的主线程白白消耗很多时间,另一方面会导致页面无法及时更新,造成页面卡顿。
所以浏览器采用异步的方法来避免,具体做法为当某些任务发生时,如定时器、网络请求、事件监听等,主线程会将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续的代码,当其他线程处理完毕后,会将事先传递的回调包装成任务,加入到任务队列(消息队列)的末尾排队,等待主线程的调度执行。
在这种异步的模式下,浏览器永不堵塞,从而最大限度的保障了单线程的执行。

五、任务队列(消息队列)的优先级

在任务队列中,依据先进先出原则执行,但浏览器为了优化用户体验,还是存在一定的优先级。
虽然宏任务队列通常没有显式的优先级,但浏览器会根据任务的类型和重要性来进行一定程度的任务调度。例如,用户交互事件通常具有较高的优先级,以确保用户体验良好。但在通常情况下,宏任务队列中的任务是按照它们的排队顺序执行的。

在目前 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级「中」
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
  • 微队列:用户存放需要最快执行的任务,优先级「最高」

六、js中的计时器能做到精确计时吗?

不行,因为

  • 计算机硬件没有原子钟,无法做到精确计时
  • 操作系统的计时函数本身就有少量的偏差,由于js的计时器最终调用的还是操作系统的计时器,也就携带了这些偏差
  • 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,则会带有 4 毫秒的最少时间,这样在计时时间少于 4 毫秒时又带来了偏差
  • 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差