参考文章:
浏览器的进程机制
在进入主题前,我们先要科普一下浏览器进程方面的知识,首先,浏览器是一个多进程模型,具体体现在:
- 每个标签页都是一个独立的进程,浏览器出于优化的考虑,有时候会把多个进程合并为一个,例如打开多个空白标签页;
- 由主进程控制多个子进程,我们所看到的浏览器用户窗口就是主进程;
- 每一个标签页都有一个渲染进程,同一域名下的网址可能共享一个渲染进程;
- 有专门的网络进程,处理请求;
- 浏览器安装的第三方插件也都有独立的线程。
多进程的优势有:
- 避免单个页面运行阻塞影响到整个浏览器;
- 避免第三方插件阻塞影响到整个浏览器;
- 多进程充分利用多核优势;
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性。
渲染进程的多线程体现
渲染进程是前端开发最关注的一个进程,它是多线程的,页面的渲染、JavaScript 的执行与解析、事件循环都在这个进程中进行。
其多线程包括:
- GUI 渲染线程:负责页面的渲染;
- JavaScript 引擎线程:负责 JavaScript 的解析与执行;
- 事件循环(EventLoop)线程:负责异步代码的调度;
- 定时触发线程:setTimeout 和 setInterval 所在的线程;
- 事件触发线程:专门处理 click、touch 等事件;
- 异步 Http 请求线程:每个 XMLHttpRequest 连接后都会新开一个线程处理网络请求。
JavaScript的单线程体现
人们常说的 JavaScript 单线程指的是 JavaScript 的主线程,即 JavaScript 引擎线程(负责 JavaScript 的解析与执行),单线程体现在:执行代码时,渲染 DOM 会被挂起,渲染 DOM 时,JavaScript 代码则会挂起。
简单来说就是,JavaScript 引擎线程与 GUI 渲染线程是互斥的。
PS:一个人打两份工可真累~
任务队列
现在进入主题,本篇的主角就是任务队列,其分为宏任务队列与微任务队列,在代码执行过程中,不同的任务类型添加到不同的队列中。既然称为“队列”,那么肯定就准寻先进先出的原则。
宏任务
宏任务是宿主环境本身提供的异步方法,包括:
- script 脚本;
- setTimeout;
- setInterval;
- Ajax;
- DOM 事件。
微任务
微任务是语言标准所提供的,包括:
- Promise;
- MutationObserver;
- V8 的垃圾回收;
- Node 独有的 process.nextTick;
事件循环的机制
事件循环的每一次循环,称为 tick,机制如下:
- JavaScript 引擎线程中的执行栈首先执行宏任务(一般是 script),执行完所有的同步代码;
- 代码的执行过程中一定会有同步代码和异步代码,异步代码根据不同的任务类型,相应的回调函数会添加到宏任务队列和微任务队列中;
- 宏任务执行完后,检查微任务队列,清空微任务队列,执行完所有微任务;
- 微任务队列清空后,如果宿主为浏览器,可能会渲染页面,浏览器也会相应的优化,多个 tick 后合并成一次渲染页面;
- 开始下一轮 tick,宏任务队列中拿出一个宏任务(注意是一个宏任务,setTimeout等回调)执行。
示例
以下是搬运的两个示例:
1)
<script>
Promise.resolve().then(() => {
console.log('Promise1')
setTimeout(() => {
console.log('setTimeout2')
}, 0);
})
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve().then(() => {
console.log('Promise2')
})
}, 0);
</script>
运行结果:Promise1,setTimeout1,Promise2,setTimeout2
解析:
- script 整体会被看做成一个宏任务,进入执行栈,执行完所有同步代码;
- 代码中会包含异步代码,对应回调函数添加到相应的队列中;
- 外层的 Promise 的 then 方法是微任务,Promise1 回调添加到微任务队列, 外层的 setTimeout 是宏任务,setTimeout1 回调添加到宏任务队列;
- script(宏任务)执行完后,清空微任务队列,执行 Promise1 回调,执行过程中发现有个 setTimeout,setTimeout2 回调添加到宏任务队列;
- 接着页面渲染,可能会渲染,也可能合并成一次;
- 宏任务队列中取出一个宏任务执行,也就是执行 setTimeout1 回调,执行过程中发现有个 Promise,Promise2 回调添加到微任务队列;
- 再次清空微任务队列,执行 Promise2 回调;
- 页面渲染,也可能不会渲染;
- 再次宏任务队列中取出一个宏任务执行,执行 setTimeout2 回调。
2)
<script>
Promise.resolve().then(function F1() {
console.log('promise1')
Promise.resolve().then(function F4() {
console.log('promise2');
Promise.resolve().then(function F5() {
console.log('promise4');
}).then(function F6() {
console.log('promise7');
})
}).then(function F7() {
console.log('promise5');
})
}).then(function F2() {
console.log('promise3');
}).then(function F3() {
console.log('promise6');
})
</script>
运行结果:promise1,promise2,promise3,promise4,promise5,promise6,promise7
解析:
- 首先这段代码放一个 script 脚本中就是一个宏任务,会首先执行这个宏任务;
- 然后发现有微任务代码也就是最外层的(Promise.resolve().then())方法;
- 会把最外层的 then 方法的回调(F1)添加到微任务队列;
- 前面的宏任务执行完了(也就是整个 script 脚本),会马上清空微任务队列,清空也就是依次执行任务队列的回调;
- 执行 F1 打印 promise1,然后 F1 中有一个微任务 F4,F4 添加到微任务队列,这时候 F1 执行完毕,会调用外层的第二个 then 方法,F2 添加到微任务队列;
- 再次清空微任务队列,依次执行 F4、F2。 F4 中又发现个微任务 F5,F5 添加到微任务队列,F4 执行完毕后,会执行内层的 then 方法,F7 添加到队列。F2 执行完毕后,会调用外层的第三个 then,F3 添加到微任务队列;
- 再次清空微任务队列,依次执行 F5、F7、F3。 F5 执行完毕后,会执行最里层的then,F6 添加到微任务队列;
- 再次清空微任务队列 执行 F6。
数组模拟微任务队列:[F1]、[F4, F2]、[F5, F7, F3]、[F6]。