V8
一、前言
1.1 什么是线程?
谷歌浏览器采用多进程架构,其中JavaScript
位于渲染进程。渲染进程除了JavaScript 引擎线程外,还有GUI 渲染线程
、事件触发线程
、定时器触发线程
、异步请求触发线程
,它们的工作如下:
-
GUI渲染线程
-
负责渲染页面,布局和绘制
-
页面需要重绘和回流时,该线程就会执行
-
与js引擎线程互斥,防止渲染结果不可预期
-
-
JS引擎线程
-
负责处理解析和执行
javascript
脚本程序 -
只有一个
JS
引擎线程(单线程) -
与
GUI
渲染线程互斥,防止渲染结果不可预期
-
-
事件触发线程
-
用来控制事件循环(鼠标点击、
setTimeout
、ajax
等) -
当事件满足触发条件时,将事件放入到
JS
引擎所在的执行队列中
-
-
定时触发器线程
-
setInterval
与setTimeout
所在的线程 -
定时任务并不是由
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