跳到主要内容

V8

2024年04月07日
柏拉文
越努力,越幸运

一、前言


1.1 什么是线程?

谷歌浏览器采用多进程架构,其中JavaScript位于渲染进程渲染进程除了JavaScript 引擎线程外,还有GUI 渲染线程事件触发线程定时器触发线程异步请求触发线程,它们的工作如下:

  • GUI渲染线程

    • 负责渲染页面,布局和绘制

    • 页面需要重绘和回流时,该线程就会执行

    • 与js引擎线程互斥,防止渲染结果不可预期

  • JS引擎线程

    • 负责处理解析和执行javascript脚本程序

    • 只有一个JS引擎线程(单线程)

    • GUI渲染线程互斥,防止渲染结果不可预期

  • 事件触发线程

    • 用来控制事件循环(鼠标点击、setTimeoutajax等)

    • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中

  • 定时触发器线程

    • setIntervalsetTimeout所在的线程

    • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的

    • 计时完毕后,通知事件触发线程

  • 异步http请求线程

    • 浏览器有一个单独的线程用于处理AJAX请求

    • 当请求完成时,若有回调函数,通知事件触发线程

1.2 为什么 javascript 是单线程的?

首先是历史原因,在创建 Javascript 这门语言时,多进程多线程的架构并不流行,硬件支持并不好。其次是因为多线程的复杂性,多线程操作需要加锁,编码的复杂性会增高。而且,如果同时操作DOM ,在多线程不加锁的情况下,最终会导致 DOM 渲染的结果不可预期。

1.3 为什么 GUI 渲染线程与 JS 引擎线程互斥

这是由于JavaScript是可以操作DOM的,如果同时修改元素属性并同时渲染界面(即 JS线程和UI线程同时运行), 那么渲染线程前后获得的元素就可能不一致了。因此,为了防止渲染出现不可预期的结果,浏览器设定GUI渲染线程和JavaScript引擎线程为互斥关系, 当JavaScript引擎线程执行时GUI渲染线程会被挂起,GUI更新则会被保存在一个队列中等待JS引擎线程空闲时立即被执行。

二、认识


JavaScript 代码执行过程中,会创建对应的执行上下文并压入执行上下文栈中。如果遇到异步任务就会将任务挂起,交给其他线程去处理异步任务,当异步任务处理完后,会将回调结果加入事件队列中。当执行栈中所有任务执行完毕后,就是主线程处于闲置状态时,才会从事件队列中取出排在首位的事件回调结果,并把这个回调加入执行栈中然后执行其中的代码,如此反复,这个过程就被称为事件循环

事件队列分为了宏任务队列微任务队列, 在当前执行栈为空时,主线程回先查看微任务队列是否有事件存在,存在则依次执行微任务队列中的事件回调,直至微任务队列为空;不存在再去宏任务队列中处理。

三、过程


Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

JavaScript 分为同步任务异步任务,同步任务都在主线程上执行,形成一个执行栈事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中。执行栈中所有同步任务执行完毕,此时JavaScript 引擎进程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行。

在开发中,通过setTimeout/setInterval 来指定定时任务,通过XHR/Fetch发送网络请求,它们自身是同步任务,其中的回调函是异步任务。

  • 当执行到setTimeout/setInterval: JavaScript 引擎线程通过定时触发线程,间隔一个时间后触发一个回调事件,而定时触发器线程收到消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列

  • 当执行到XHR/Fetch: JavaScript 引擎线程通知异步请求线程,发送一个网络请求,并制定请求完成后的回调事件,而异步请求线程收到消息后,会在请求成功后,将回调事件放入到由事件触发线程所管理的事件队列中。

当同步任务执行完,JavaScript 引擎线程会询问事件触发线程,在事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JavaScript 引擎线程执行。

根据以上基础的铺设,得出事件循环的过程:

3.1 执行宏任务

每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),如果执行栈中没有,就从事件队列

3.2 添加微任务

执行宏任务的过程中如果遇到微任务,就将它添加到微任务的任务队列

3.3 执行微任务

每一个宏任务执行完毕,检查微任务队列。执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行

3.4 页面渲染

当前宏任务宏任务所产生的的微任务 完整的执行完毕后,由于JavaScript 引擎线程GUI 渲染线程 是互斥的关系,浏览器为了能够使宏任务DOM 任务有序的进行,在下一个宏任务执行前,GUI 渲染线程 开始工作,对页面进行渲染

3.5 判断是否渲染

进入页面更新渲染阶段,判断是否需要渲染。这里有一个 rendering opportunity。也就是说不一定每一轮Event Loop都会对应一次浏览器渲染,要根据屏幕刷新率页面性能页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。(所以多个 task很可能在一次渲染之间执行---合并渲染)判断逻辑如下:

  • 1. 浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps(每 16.66ms 渲染一次)的话,那么浏览器就会选择 30fps 的更新速率,而不是偶尔丢帧

  • 2. 如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低

  • 3. 如果满足以下条件,也会跳过渲染:

    • a.浏览器判断更新渲染不会带来视觉上的改变

    • b.map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求帧动画

3.6 需要渲染之后

通过第五步判断出页面需要渲染,那么继续执行之后的操作

3.7 执行 resize 回调

对于需要渲染的文档,如果窗口的大小发生了变化,执行监听的 resize 方法**: 对于resieze 而言,并不是到了这一步才去执行滚动和缩放,浏览器当然会立刻帮你滚动视图,浏览器会保存一个 pending resize event targets,等到事件循环中的 resize 这一步,去派发一个事件到对应的目标上,驱动它去执行监听的回调函数而已

3.8 执行 scroll 回调

对于需要渲染的文档,如果页面发生了滚动,执行 scroll 方法**: 对于scroll 而言,并不是到了这一步才去执行滚动和缩放,浏览器当然会立刻帮你滚动视图,浏览器会保存一个 pending scroll event targets,等到事件循环中的 scroll 这一步,去派发一个事件到对应的目标上,驱动它去执行监听的回调函数而已

3.9 执行 requestAnimationFrame 回调

对于需要渲染的文档,执行帧动画回调,也就是 requestAnimationFrame 的回调requestAnimationFrame 回调有如下特点:

  • requestAnimationFrame 回调在哪个阶段执行? ==> 重新渲染之前执行

    • 为什么要在重新渲染前去调用?: 因为 requestAnimationFrame 是官方推荐的用来做一些流畅动画所应该使用的 API,做动画不可避免的会去更改 DOM,而如果在渲染之后再去更改 DOM,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这显然是不合理的。requestAnimationFrame 在浏览器决定渲染之前给你最后一个机会去改变 DOM 属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择
  • requestAnimationFrame 回调很可能在宏任务之后不调用

3.10 执行 IntersectionObserver 回调

对于需要渲染的文档, 执行 IntersectionObserver 的回调

3.11 重新渲染绘制用户界面

对于需要渲染的文档,重新渲染绘制用户界面

3.12 执行 requestIdleCallback 回调

判断执行栈微任务队列是否都为空,如果是的话,则进行 Idle 空闲周期的算法,判断是否要执行 requestIdleCallback 的回调函数

四、特点


4.1 Macro-Task (宏任务)

宏任务有明确的异步任务需要执行和回调,需要其他异步线程支持

4.2 Micro-Task (微任务)

微任务没有明确的异步任务需要执行,只有回调,不需要其他异步线程支持。

五、任务


5.1 Macro-Task (宏任务)

  • I/O

  • UI 交互事件

  • setTimeout

  • setInterval

  • setImmediate

  • MessageChannel

5.2 Micro-Task (微任务)

  • Promise.then

  • Object.observe

  • process.nextTick

  • MutationObserver

参考资料


Node.js 事件循环机制

「进击的前端工程师」一文带你搞懂JavaScript事件循环

又被node的eventloop坑了,这次是node的锅

一次弄懂Event Loop

「前端进阶」从多线程到Event Loop全面梳理

深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)