参考文章:

  1. 事件循环(EventLoop)
  2. 事件循环
  3. 浏览器的多进程

浏览器的进程机制

在进入主题前,我们先要科普一下浏览器进程方面的知识,首先,浏览器是一个多进程模型,具体体现在:

  1. 每个标签页都是一个独立的进程,浏览器出于优化的考虑,有时候会把多个进程合并为一个,例如打开多个空白标签页;
  2. 由主进程控制多个子进程,我们所看到的浏览器用户窗口就是主进程;
  3. 每一个标签页都有一个渲染进程,同一域名下的网址可能共享一个渲染进程;
  4. 有专门的网络进程,处理请求;
  5. 浏览器安装的第三方插件也都有独立的线程。

多进程的优势有:

  1. 避免单个页面运行阻塞影响到整个浏览器;
  2. 避免第三方插件阻塞影响到整个浏览器;
  3. 多进程充分利用多核优势;
  4. 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性。

渲染进程的多线程体现

渲染进程是前端开发最关注的一个进程,它是多线程的,页面的渲染、JavaScript 的执行与解析、事件循环都在这个进程中进行。

其多线程包括:

  1. GUI 渲染线程:负责页面的渲染;
  2. JavaScript 引擎线程:负责 JavaScript 的解析与执行;
  3. 事件循环(EventLoop)线程:负责异步代码的调度;
  4. 定时触发线程:setTimeout 和 setInterval 所在的线程;
  5. 事件触发线程:专门处理 click、touch 等事件;
  6. 异步 Http 请求线程:每个 XMLHttpRequest 连接后都会新开一个线程处理网络请求。

JavaScript的单线程体现

人们常说的 JavaScript 单线程指的是 JavaScript 的主线程,即 JavaScript 引擎线程(负责 JavaScript 的解析与执行),单线程体现在:执行代码时,渲染 DOM 会被挂起,渲染 DOM 时,JavaScript 代码则会挂起。

简单来说就是,JavaScript 引擎线程与 GUI 渲染线程是互斥的。
image.png
PS:一个人打两份工可真累~

任务队列

现在进入主题,本篇的主角就是任务队列,其分为宏任务队列与微任务队列,在代码执行过程中,不同的任务类型添加到不同的队列中。既然称为“队列”,那么肯定就准寻先进先出的原则。

宏任务

宏任务是宿主环境本身提供的异步方法,包括:

  • script 脚本;
  • setTimeout;
  • setInterval;
  • Ajax;
  • DOM 事件。

微任务

微任务是语言标准所提供的,包括:

  • Promise;
  • MutationObserver;
  • V8 的垃圾回收;
  • Node 独有的 process.nextTick;


事件循环的机制

事件循环的每一次循环,称为 tick,机制如下:

  • JavaScript 引擎线程中的执行栈首先执行宏任务(一般是 script),执行完所有的同步代码;
  • 代码的执行过程中一定会有同步代码和异步代码,异步代码根据不同的任务类型,相应的回调函数会添加到宏任务队列和微任务队列中;
  • 宏任务执行完后,检查微任务队列,清空微任务队列,执行完所有微任务;
  • 微任务队列清空后,如果宿主为浏览器,可能会渲染页面,浏览器也会相应的优化,多个 tick 后合并成一次渲染页面;
  • 开始下一轮 tick,宏任务队列中拿出一个宏任务(注意是一个宏任务,setTimeout等回调)执行。

JavaScript EventLoop解析 - 图2

示例

以下是搬运的两个示例:

1)

  1. <script>
  2. Promise.resolve().then(() => {
  3. console.log('Promise1')
  4. setTimeout(() => {
  5. console.log('setTimeout2')
  6. }, 0);
  7. })
  8. setTimeout(() => {
  9. console.log('setTimeout1');
  10. Promise.resolve().then(() => {
  11. console.log('Promise2')
  12. })
  13. }, 0);
  14. </script>
  15. 运行结果:Promise1,setTimeout1,Promise2,setTimeout2

解析:

  • script 整体会被看做成一个宏任务,进入执行栈,执行完所有同步代码;
  • 代码中会包含异步代码,对应回调函数添加到相应的队列中;
  • 外层的 Promise 的 then 方法是微任务,Promise1 回调添加到微任务队列, 外层的 setTimeout 是宏任务,setTimeout1 回调添加到宏任务队列;
  • script(宏任务)执行完后,清空微任务队列,执行 Promise1 回调,执行过程中发现有个 setTimeout,setTimeout2 回调添加到宏任务队列;
  • 接着页面渲染,可能会渲染,也可能合并成一次;
  • 宏任务队列中取出一个宏任务执行,也就是执行 setTimeout1 回调,执行过程中发现有个 Promise,Promise2 回调添加到微任务队列;
  • 再次清空微任务队列,执行 Promise2 回调;
  • 页面渲染,也可能不会渲染;
  • 再次宏任务队列中取出一个宏任务执行,执行 setTimeout2 回调。

2)

  1. <script>
  2. Promise.resolve().then(function F1() {
  3. console.log('promise1')
  4. Promise.resolve().then(function F4() {
  5. console.log('promise2');
  6. Promise.resolve().then(function F5() {
  7. console.log('promise4');
  8. }).then(function F6() {
  9. console.log('promise7');
  10. })
  11. }).then(function F7() {
  12. console.log('promise5');
  13. })
  14. }).then(function F2() {
  15. console.log('promise3');
  16. }).then(function F3() {
  17. console.log('promise6');
  18. })
  19. </script>
  20. 运行结果: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]。