深入浅出、面面俱到,很推荐这篇文章:
掘金_「硬核JS」一次搞懂JS运行机制https://juejin.cn/post/6844904050543034376#heading-15
一、行进与线程
1. 进程:
- “CPU资源分配的最小单位”
- 一个进程 = 该程序 + 该程序使用的内存 + 该程序使用的系统资源
- 一个CPU在一个时间点只能运行一个进程,借助 “时间片轮转调度算法” 实现多个进程之间的切换,以达到同时运行多个进行的目的
2. 线程:
- “CPU调度的最小单位”
- 不同的线程表示一个进程中不同的执行路线
- 一个进程可以有多个线程
- 独有资源:栈、寄存器(物理寄存器的副本)
- 共享资源:堆、全局变量、静态变量、文件等公用资源
- 共享的环境:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录、进程用户ID、进程组ID(操作系统--多线程之间共享哪些资源?)
拓展:虚拟线程
- 定义:
虚拟线程是一种逻辑上的线程,它并不直接对应于操作系统的物理线程。由Java虚拟机在内部管理和调度,在一个物理线程上可以创建多个虚拟线程,并通过合理的调度算法实现并发执行
- 原理:
基于Java虚拟机的线程调度和上下文切换机制。当虚拟线程被创建时,它会被分配给一个物理线程进行执行。虚拟线程的调度由Java虚拟机负责,根据线程优先级、调度策略和资源可用性等因素进行决策。
虚拟线程的上下文切换是通过保存和恢复线程上下文实现的。当一个虚拟线程的执行时间片用完或者发生阻塞时,Java虚拟机会保存该线程的上下文状态,并切换到其他可执行的虚拟线程。当虚拟线程再次被调度执行时,它的上下文状态将被恢复,从上次离开的地方继续执行。
3. 进程与线程的区别:
- 进程之间相互独立,但同一进程下各个线程之间共享程序的内存以及程序级资源
4. 多进程与多线程:
- 多线程意味着在一个程序中,同一时间可以同时执行多个任务
- 多进程意味着可以在同一时间运行多个程序
5. 单线程的JS:
- 尽管借助Web Worker可以为JS创建多个线程,但这些线程都受到主线程的控制,因此本质上依旧是单线程
二、浏览器
1. 浏览器是多进程的
一个Tab页就是一个进程,首页也很消耗CPU
2. 浏览器都有哪些进程?
-
Browser进程
- 浏览器的主进程(负责界面显示、交互、管理,网络资源的管理等),该进程只有一个
-
第三方插件进程
- 每种类型的插件对应一个进程,使用该插件时才创建
-
GPU进程
- 该进程也只有一个,用于3D绘制等等
-
渲染进程(重)
- 也就是浏览器内核(Renderer进程,内部是多线程)
- 每个Tab页面都有一个渲染进程,互不影响,负责页面渲染,脚本执行,事件处理等
3. 为什么浏览器是多进程?
- 避由于某个插件或Tab页的异常影响整个浏览器
三、渲染进程(Renderer)
渲染进程的主要线程:
- GUI渲染线程
- 解析HTML、CSS生成DOM Tree、CSSOM,再组装成Render Tree
- 重绘、回流时,对页面的绘制
- GUI渲染线程与JS引擎线程是互斥的
- 当JS引擎执行时,GUI线程会被挂起(相当于冻结了)。GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行
- JS引擎线程
- 也就是JS内核(例如Chrome的V8引擎),负责解析JavaScript脚本,管理着一个 “执行栈”,一个“微任务队列”
- 事件触发线程
- 控制 “事件循环”,管理着一个 “宏任务队列”(event queue)
- 当JS执行时,如果遇到 “异步任务”,“事件触发线程” 将任务添加到对应的线程中(比如定时器操作,便把定时器任务添加到定时器线程),等异步任务有了结果,便把它们的回调添加到 “宏任务队列”,等待JS引擎线程空闲时处理
- 定时触发器线程(setTimeout、setInterval)
- 执行到定时器时,先在 “定时器线程” 进行 “定时” 与 “计时”,计时完毕后,将回调函数交给 “事件触发线程”,再由 “事件触发线程” 将回调函数添加到 “任务队列” 中,等待JS引擎线程空闲后执行
- W3C规定,setTimeout中小于4ms的事件间隔算为4ms
- 异步http请求线程
- 执行到一个http请求时,先把 “异步请求任务” 添加到 “异步请求线程” 中,请求状态变更后,把回调交给 “事件触发线程”,由 “事件触发线程” 将回调函数添加到 “任务队列”
我们会发现,浏览器上所有线程的工作都很单一且独立,非常符合 “单一原则”。
“定时触发线程” 只管理定时器时只关注定时,不关心结果,定时结束就把回调扔给“事件触发线程”;
“异步http请求线程” 只管理http请求,同样不关心回调对应的结果,请求成功后就把回调扔给 “事件触发线程”;
“事件触发线程” 只关心将异步回调推入任务队列。
四、事件循环(Event Loop)
1. 从“同步”、“异步”的角度看事件循环
script分为 “同步任务” 与 “异步任务”,“同步任务” 在 “执行栈” 中执行,“异步任务” 有了运行结果,就将其回调放入 “任务队列” 中。“执行栈” 中的 “同步任务” 全部执行完毕,就开始读取 “任务队列”,依次添加到 “执行栈” 中执行。
注意,await以前的代码,相当于new Promise的同步代码,await以后的代码相当与Promise.then的异步。
2. 从“宏任务”、“微任务”角度看事件循环
注意!宏任务(除最大的 script 外)、微任务 都是异步任务!
1)宏任务(macrotask)
宏任务的每次每次执行都是从头到尾地执行,不会在中途停止下来去执行其他任务。
常见的宏任务:
- script代码
- setTimeout
- setInterval
- requestAnimationFrame
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:
- requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
- 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。
2)微任务(microtask)
常见微任务
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
3)Event Loop流程
当一个 “宏任务” 执行完,会在渲染前,将执行期间所产生的所有 “微任务” 都执行完。然后执行下一个 “宏任务”,如此循环往复。
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
渲染时,在一个GUI线程中,会将所有UI改动优化合并。
注意,并不是每一个微任务执行完毕过后都会进行渲染,而是按照帧的频率处理渲染操作。每一帧内可能循环多次,浏览器每一帧才会渲染一次页面,因此,一帧内的所有渲染最终会合并为一次。
3. 结合“同步任务”、“异步任务”、“宏任务”、“微任务”分析事件循环
- 首先,“微任务” 的事件回调在 “JS引擎线程” 管理的 “微任务队列” 中;“宏任务” 的事件回调在 “事件触发线程” 管理的 “宏任务队列(消息队列)” 中
- <script>是最大的 “宏任务”,也是一个宏任务中唯一的一个 “同步任务”,在主线程(JS引擎线程)中执行
- 执行时,若遇到 “异步任务”,判断 “异步任务” 是 “微任务” 还是 “宏任务”
- 宏任务进入到 “事件表”(Event Table)中,并在里面注册回调函数
- 若是 “宏任务”,由事件触发线程来判断这个 “宏任务” 是否拥有自己的 “专属执行线程”
- 有,就把该任务交给 “专属线程”,专属线程完成对应事件后,将事件对应的回调再转交给 “事件触发线程”,事件触发线程再将 “事件表” 中对应的回调函数移到 “宏任务队列” 中
- “微任务” 也会进入到另一个 “事件表” 中,并在里面注册回调函数,每当指定的事件完成时,由JS引擎线程将 “事件表” 中对应的回调函数移到 “微任务队列” 中
- “主线程” 执行完<script>后,开始执行 “全局执行上下文” 管理的 “微任务队列”
- “微任务队列” 执行完,再开始执行下一个 “宏任务”,一直这样循环下去,便是事件循环Event Loop了。
我们一起来看一道面试题:
async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}
console.log('4');
setTimeout(() => {
console.log('5');
}, 0)
async1();
new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('7');
})
这道题重点考察了Promise、async/await、setTimeout的任务类型。
可能有一定基础的小伙伴很快就可以回答出来,Promise的.then属于微任务,setTimeout属于宏任务。
那么,await 关键字后面的语句是什么类型?await 下面的语句又属于什么任务类型?
我们通过答案来反推一下(一定要先尝试写一下自己的答案哈):
4
1
3
6
2
7
5
和你的答案一样吗?
我们一起来推理一下,首先打印 4 1 这很好理解,同步任务,自然是直接执行。
后面紧接着打印了 3,就表示 await 关键字引导的语句,属于同步任务,并没有被归为异步任务。await 仅阻塞下面的语句。
紧接着打印了 6,这点很简单,因为 new Promise 的部分依旧属于同步任务。
大家都知道 .then 是异步任务中的微任务,2 在 7 前面被打印,自然就可以推导出,await 下面的语句,也属于微任务。
为什么呢?本质上,await 下面的语句,其实就相当于 .then 里面的语句。同样是 .then 自然 2 就在 7 前面被打印了。
最后打印宏任务的定时器回调。