Event Loop总结

| 字数 2175
  1. 1. 1、进程与线程的区别
    1. 1.1. 1、什么是进程?
    2. 1.2. 2、什么是线程?
  2. 2. 2、浏览器是多进程
    1. 2.1. 1. 主进程:
    2. 2.2. 2. 浏览器渲染进程
    3. 2.3. 3. GPU进程:用于3D绘制等
    4. 2.4. 4. 第三方插件进程:使用插件时创建
  3. 3. 3、Javascript 的单线程
    1. 3.0.1. 1. JS 线程(引擎线程,即常说的那个js单线程)
    2. 3.0.2. 2. GUI 渲染线程
    3. 3.0.3. 3. 事件触发线程:遵循先进先出的原则在异步队列中等待 js 线程处理。(异步队列下面会解释)
    4. 3.0.4. 4. 定时器触发线程
    5. 3.0.5. 5. http 请求线程:请求成功后会把回调放入异步队列中。
    6. 3.0.6. 6. 备注:上述执行环境是在浏览器中,如果执行环境换为 node 的话,还有 I/O 线程与 setImmediate
  • 4. 3、为什么说 JS 线程与 GUI 渲染线程互斥(js 代码会阻塞渲染)?
  • 5. 4、异步的实现方案 - event loop
  • 6. 5.总结
    1. 1. 1、进程与线程的区别
      1. 1.1. 1、什么是进程?
      2. 1.2. 2、什么是线程?
    2. 2. 2、浏览器是多进程
      1. 2.1. 1. 主进程:
      2. 2.2. 2. 浏览器渲染进程
      3. 2.3. 3. GPU进程:用于3D绘制等
      4. 2.4. 4. 第三方插件进程:使用插件时创建
    3. 3. 3、Javascript 的单线程
      1. 3.0.1. 1. JS 线程(引擎线程,即常说的那个js单线程)
      2. 3.0.2. 2. GUI 渲染线程
      3. 3.0.3. 3. 事件触发线程:遵循先进先出的原则在异步队列中等待 js 线程处理。(异步队列下面会解释)
      4. 3.0.4. 4. 定时器触发线程
      5. 3.0.5. 5. http 请求线程:请求成功后会把回调放入异步队列中。
      6. 3.0.6. 6. 备注:上述执行环境是在浏览器中,如果执行环境换为 node 的话,还有 I/O 线程与 setImmediate
  • 4. 3、为什么说 JS 线程与 GUI 渲染线程互斥(js 代码会阻塞渲染)?
  • 5. 4、异步的实现方案 - event loop
  • 6. 5.总结
  • 之前也有 event loop 的概念,但一直都是琐碎的。直到阅读以下三篇文章后,琐碎的点串在了一起。

    从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

    这一次,彻底弄懂 JavaScript 执行机制

    Event Loop的规范和实现

    为了印象深刻,还是决定自己写一写。总结如下:

    1、进程与线程的区别

    1、什么是进程?

    在计算机操作系统中,只有进程才能在系统中运行。所以要使程序运行就必须为其创建进程。当我们打开浏览器时就已经创建了进程。创建进程之后,系统会为其分配资源(内存等)供其使用。

    进程是程序(指令和数据)的真正运行实例。若干进程有可能 …

    与同一个程序相关系,且每个进程皆可以同步(循序)或异步(平行)的方式独立运行。

    2、什么是线程?

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    总的来说进程是资源分配的最小单位,线程是计算机系统调度和分派的基本单位。线程基本上不拥有资源(也有一点必不可少的资源)它只专注于调度和分派,提高系统并发程度,但是它可以访问其隶属进程的资源。

    2、浏览器是多进程

    上图来阐述,进入浏览器(chrome等)打开多个标签页,然后打开浏览器的 task manager。如下图:

    从图上来看,我们首先可以确定的是我打开的4个页面都是进程,因为它们有不同的 process id(进程标识符),这个是存在 PCB 中的,系统分配且唯一的数字标识符。PCB 是进程控制块,具体可搜索计算机操作系统-进程。

    浏览器的主要进程有四个,分别为:主进程、GPU进程、浏览器渲染(render)进程(每个标签页都会一个渲染进程)、第三方插件进程。

    主要来说说这四个进程的职责:

    1. 主进程:

    • 用户的前进、后退操作
    • 每个标签页的创建与销毁
    • 将渲染进程执行后生成的位图(bitmap)绘制在对应标签页上,呈现给用户
    • 书签、下载等功能的管理

    2. 浏览器渲染进程

    • 页面渲染
    • 脚本执行(事件、task)

    3. GPU进程:用于3D绘制等

    4. 第三方插件进程:使用插件时创建

    主要来说下浏览器渲染进程,这个进程是当前标签的控制者。js 代码的执行、事件的触发、页面的展示等都由它负责。那么这一个进程可以同时干这么多事情吗?它一个当然是可以的,但是如果一个人来做就会浪费资源而且一旦执行脚本文件后会不断阻塞页面执行。至此,引入了线程来解决这个问题。

    3、Javascript 的单线程

    在渲染进程中划分了很多个线程,这里介绍一些常驻线程:

    1. JS 线程(引擎线程,即常说的那个js单线程)
    • 运行环境(v8),负责处理 js 脚本
    • 每个标签页只有一个 js 线程在执行代码
    • js 线程与 Gui 渲染线程互斥,js 执行时间过长会导致页面渲染的阻塞(互斥原因下面阐述
    2. GUI 渲染线程
    • 负责渲染页面(构建dom 树 & cssparser 树 & renderobject 树)
    • 当页面出现重绘或回流(repaint/reflow)时,会调用该线程
    • 与 js 线程互斥
    3. 事件触发线程:遵循先进先出的原则在异步队列中等待 js 线程处理。(异步队列下面会解释)
    4. 定时器触发线程
    • 定时器并不是在某段时间后执行,而是在某时间后被添加至异步队列,等 js 线程空闲后执行
    • W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
    5. http 请求线程:请求成功后会把回调放入异步队列中。
    6. 备注:上述执行环境是在浏览器中,如果执行环境换为 node 的话,还有 I/O 线程与 setImmediate

    3、为什么说 JS 线程与 GUI 渲染线程互斥(js 代码会阻塞渲染)?

    GUI 渲染线程第一个构建的是 dom 树,js 代码也有可能会修改 dom 结构,一旦修改就会引起 重绘或回流,这就会导致 GUI 线程的再次从头开始。为了避免这样的事情发生所以采用了这种互斥的策略。俩者并不能在同一时间进行。

    4、异步的实现方案 - event loop

    针对于 js 引擎单线程这种情况,为了不让程序(被 http 请求、定时器等)阻塞,所以有了异步的概念。但是在现代 web 程序开发中,代码量越来越大。为了能让越来越多的定时器、事件、请求有序且规范的执行,event loop 方案出现了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    console.log('1');

    setTimeout(function() {
    console.log('2');
    }, 0);

    console.log('3');

    Promise.resolve().then(function() {
    console.log('4');
    }).then(function() {
    console.log('5');
    });

    console.log('6');

    上述代码的执行结果是:1 3 6 4 5 2

    首先介绍 task & microtask:

    • task 主要包括:setTimeout、setInterval、setImmediate、I/O、UI交互事件
    • microtask 主要包括:Promise、process.nextTickMutationObserver(在node中 process.nextTick 的优先级高于 promise)

    task & microtask 在当前标签页中拥有俩个队列来分别存储 task & microtask除了 js & gui 线程,其他每个线程执行时都会按照 task都有一个异步队列来控制顺序。如下图

    Event Loop

    Event Loop 过程如下:

    1. 整个script代码作为 task 执行时先判断是同步任务还是异步任务
    2. 同步任务依次进入 js 线程,依次执行
    3. 异步任务先在 table 中注册,然后当任务完成时按照 task/microtask 区分开来,依次进入不同的异步队列
    4. js 线程执行完毕后(执行栈为空)先读取 microtask 的异步队列,全部执行完毕(队列为空后),再去读取 task 的异步队列,task 异步队列中的任务若包含 microtask 则再次去读取 microtask 异步队列
    5. 上述过程不断重复,直至 task 异步队列为空。整个过程即为 Event Loop。

    还是直接来看代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    console.log('1');

    setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
    console.log('4');
    resolve();
    }).then(function() {
    console.log('5')
    })
    })
    new Promise(function(resolve) {
    console.log('7');
    resolve();
    }).then(function() {
    console.log('8')
    })

    setTimeout(function() {
    console.log('9');
    new Promise(function(resolve) {
    console.log('11');
    resolve();
    }).then(function() {
    console.log('12')
    })
    })

    详细过程请参考:

    Event Loop的规范和实现

    输出:1 7 8 2 4 5 9 11 12

    那如果执行环境切换为 node,node 的 Event loop的实现与浏览器稍有差异。具体如下:

    node - event loop

    需要注意的是:

    • expired timers and intervals queue 这个异步队列专门来存储所有的 setTimeout/setInterval
    • immediates queue 这个异步队列专门来存储所有的 setImmediate
    • next tick queue 专门来存储所有的 process.nextTick
    • node 清空 queue 时会执行所有 task(不论时 task 还是 microtask)

    那么以下代码的结果呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    console.log(1)

    setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
    console.log(4)
    resolve()
    }).then(() => {
    console.log(5)
    })
    process.nextTick(() => {
    console.log(3)
    })
    })

    new Promise(resolve => {
    console.log(7)
    resolve()
    }).then(() => {
    console.log(8)
    })

    process.nextTick(() => {
    console.log(6)
    })

    setTimeout(() => {
    console.log(9)
    process.nextTick(() => {
    console.log(10)
    })
    new Promise(resolve => {
    console.log(11)
    resolve()
    }).then(() => {
    console.log(12)
    })
    })

    js 执行栈为空,microtask queue 为空时,去检查 task queue,发现俩个 settimeout 这时依次执行。这里与浏览器环境不同,浏览器环境是从 task queue 读出一个 task 先执行,执行完后去检查 microtask queue,而 node 中是一次性执行完 task queue,再去检查 micrtask queue(比如 process.nextTick、promise 等)

    输出:1 7 6 8 2 4 9 11 3 10 5 12

    分析过程请参考:

    Event Loop的规范和实现

    5.总结

    • js 是一门单线程语言
    • event loop 是 js 的执行机制

    参考资料: