一、Event Loop 是什么


Event Loop 是浏览器或 Node 的一种协调 JavaScript 单线程运行时不会阻塞的一种机制。
为了协调事件,用户交互,脚本,渲染,网络任务等,浏览器必须使用事件循环循环机制。

二、浏览器中的进程和线程


进程与线程

  • 进程:进程是 CPU 资源分配的最小单位
  • 线程:线程是 CPU 调度的最小单位


2.1 浏览器进程

通过浏览器的任务管理器(Shift + ESC)可以看到。

主进程

  • 负责界面显示(地址栏、导航栏、书签等)、处理用户事件、管理⼦进程等。


渲染进程

  • 浏览器会为每个标签页单独启动一个渲染进程,并不是唯一的。
  • 任务:将 HTML、CSS 和 JavaScript 转化为⽤户可以与之交互的网页,包括:
    1. 渲染引擎线程
      • 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
    2. JavaScript 引擎线程
      • 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
    3. 事件触发线程
      • 负责接收事件,并将回调函数放入 JavaScript 引擎线程的事件队列中;
      • 负责处理定时任务的定时器线程。


GPU 进程

  • 处理来自其他进程的 GPU 任务,如:
    • 来自渲染进程或扩展程序进程的 CSS3 动画效果
    • 来自浏览器进程的界面绘制等。
    • 浏览器渲染页面的过程,最后一个步骤“绘制”中的图层合成,就是交给 GPU 进程来完成的。
  • 可以利用 GPU 硬件来加速渲染,包括:

    • Canvas 绘制、
    • CSS3 转换(Transitions)、CSS3 变换(Transforms)
    • WebGL
    • 具体原理: 如果 DOM 元素使用了这些属性,GPU 进程就会在合成层的时候对它进行单独处理,提升到一个独立的层进行绘制,这样就能避免重新布局和重新绘制。

      Network Service 进程

  • 负责⻚⾯**的⽹络资源加载**,比如在地址栏输入一个网页地址,网络进程会将请求后得到的资源交给渲染进程处理。

  • 本来只是浏览器主进程的一个模块,现在为了将浏览器进程进行“服务化”,被抽取出来,成了一个单独的进程。


V8 代理解析工具进程

Chrome 支持使用 JavaScript 来写连接代理服务器脚本,称为 pac 代理脚本。

扩展程序进程

主要是负责插件的运⾏,和渲染进程一样,也不是唯一的。

2.2 JS 的单线程

个人认为 JavaScript 的单线程是指 JavaScript **引擎**是单线程的,JavaScript 的引擎并不是独立运行的,跨平台意味着 JavaScript 依赖其运行的宿主环境 —- 浏览器。

  • 浏览器需要渲染 DOM,JavaScript 可以修改 DOM 结构,
  • JavaScript 执行时,浏览器 DOM 渲染停止。

如果 JavaScript 引擎线程不是单线程的,那么可以同时执行多段 JavaScript,如果这多段 JavaScript 都操作 DOM,那么就会出现 DOM 冲突

2.3 JS 的异步

同步与异步

同步和异步描述的是进程/线程调用方式

  • 同步调用指的是进程/线程发起调用后,一直等待**调用**返回后继续执行下一步操作;
    • 但这并不代表CPU在这段时间内也会一直等待,操作系统多半会切换到另一个进程线程上去,等

调用返回后再切换回原来的进程/线程

  • 异步指发起调用后,进程/线程继续向下执行,当调用返回后,通过某种手段通知调用者。
    • 回调函数是最好的接受异步调用返回数据的方式的;

注意:同步和异步中的“调用返回”,是指内核进程将数据复制到调用进程( Linux环境下)。而纵观下来,回调函数是最好的接受异步调用返回数据的方式的。

常说的:Javascript是一门异步的语言,但 ECMAScript 里并没有关于异步的规范。
Javascript 的异步更多是依靠浏览器 Runtime内部其他线程来实现 , 并非 Javascript本身的功能,
浏览器提供的支持让 Javascript看起来像是一个异步的语言**

JS中,大部分事件是同步的,少数是异步的:
浏览器定时器事件绑定ajax/fetchpromiseasync/await
Node.jsprocess.nextTicksetImmediateFS进行I/O操作
**

三、浏览器的 Event Loop

事件循环 - 图1

上图是一张 JS 的运行机制图,Js 运行时大致会分为几个部分:

  • Call Stack:函数调用栈(执行栈)

    • 当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈。
    • 后面每遇到一个函数调用,就会往栈中压入一个新的函数上下文。
    • JS引擎会执行栈顶的函数,执行完毕后,弹出对应的上下文;
  • Task Queue:任务队列

    • 宏任务(macro-task)队列
      • setTimeoutsetIntervalsetImmediatescript(整体代码)、I/O操作
    • 微任务(micro-task)队列
      • process.nextTickPromiseasync/await MutationObserver


3.1 执行过程:

Event Loop 也可以理解为:不断地从**任务队列中取出任务到调用栈**执行的一个过程
image.png

  1. 执行并出队一个 **macro-task**
    • 初始状态时:调用栈空,micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。这时首先执行并出队的就是 script 脚本


  1. 全局上下文(script 标签)被推入调用栈,同步代码执行
    • 在执行的过程中,通过对一些接口(**WebAPIs**)的调用,可以产生新的 macro-taskmicro-task,它们会分别被推入各自的任务队列里。

这个过程本质上是:队列的 macro-task 的执行和出队的过程

注意:
JavaScript 的代码执行时,同步任务会被依次加入执行栈中先执行;
而对于异步任务,其是在拿到结果的时候,才将注册的**回调函数**放入任务队列,这一步由
**事件触**``**发线程**进行监听,而不是说注册一个异步任务就会被放在这个任务队列中。

  1. 上面操作中,出队了一个 macro-task这一步出队micro-task,

    但要注意:

    • macro-task 出队时,任务是一个一个执行的;
    • micro-task 出队时,任务是一队一队执行的(如下图所示)。

因此,处理 micro 队列时,会逐个执行队列中的任务并把它出队,直到队列被清空

  1. 执行渲染操作,更新界面
  2. 检查是否存在 Web worker 任务,如果有,则对其进行处理。

3.2 宏任务和微任务

任务队列是一个消息队列,先进先出,后来的事件都是被加在队尾,等到前面的事件执行完了才会被执行。而如果在执行的过程中突然有重要的任务是无法得到及时处理的。
因此就产生了宏任务和微任务,微任务使得一些异步任务得到及时的处理

形象点来说:

  • 去营业厅办业务时会需要排队,当叫到你的号码的时,就去窗口办充值业务(宏任务执行),
  • 在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不会让你重新排队。
  • 宏任务的真面目是浏览器派发,与 JS 引擎无关的,参与了 Event Loop 调度的任务


  • 微任务是在运行宏任务/同步任务的时候产生的,是属于当前任务的,所以它不需要浏览器的支持,不会经过Web APIs,内置在 JS 当中,直接在 JS 的引擎中就被执行掉了。

3.3 总述:

每次的执行过程,其实都是一个宏观任务,我们可以大概理解:宏观任务的队列就相当于事件循环
在宏观任务中,JavaScript 的 Promise等还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列
image.png

3.4 回调补充

回调


**