一、Event Loop 是什么
Event Loop 是浏览器或 Node 的一种协调 JavaScript 单线程运行时不会阻塞的一种机制。
为了协调事件,用户交互,脚本,渲染,网络任务等,浏览器必须使用事件循环循环机制。
二、浏览器中的进程和线程
进程与线程
- 进程:进程是 CPU
资源分配的最小单位 - 线程:线程是 CPU
调度的最小单位
2.1 浏览器进程
主进程
- 负责界面显示(地址栏、导航栏、书签等)、处理用户事件、管理⼦进程等。
渲染进程
- 浏览器会为每个标签页单独启动一个渲染进程,并不是唯一的。
- 任务:将 HTML、CSS 和 JavaScript 转化为⽤户可以与之交互的网页,包括:
- 渲染引擎线程
- 与 JS 引擎线程互斥,也就是所谓的 JS 执行阻塞页面更新
- JavaScript 引擎线程
- 与 GUI 渲染线程互斥,执行时间过长将阻塞页面的渲染
- 事件触发线程
- 负责接收事件,并将回调函数放入 JavaScript 引擎线程的事件队列中;
- 负责处理定时任务的定时器线程。
- 渲染引擎线程
GPU 进程
- 处理来自其他进程的 GPU 任务,如:
- 来自渲染进程或扩展程序进程的 CSS3 动画效果
- 来自浏览器进程的界面绘制等。
- 浏览器渲染页面的过程,最后一个步骤“绘制”中的图层合成,就是交给 GPU 进程来完成的。
可以利用 GPU 硬件来加速渲染,包括:
负责⻚⾯**的⽹络资源加载**,比如在地址栏输入一个网页地址,网络进程会将请求后得到的资源交给渲染进程处理。
- 本来只是浏览器主进程的一个模块,现在为了将浏览器进程进行“服务化”,被抽取出来,成了一个单独的进程。
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/fetch、promise、async/await
Node.js:process.nextTick、setImmediate、FS进行I/O操作
**
三、浏览器的 Event Loop

上图是一张 JS 的运行机制图,Js 运行时大致会分为几个部分:
Call Stack:函数调用栈(执行栈)
- 当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入调用栈。
- 后面每遇到一个函数调用,就会往栈中压入一个新的函数上下文。
- JS引擎会执行栈顶的函数,执行完毕后,弹出对应的上下文;
Task Queue:任务队列
- 宏任务(macro-task)队列
setTimeout、setInterval、setImmediate、script(整体代码)、I/O操作
- 微任务(micro-task)队列
process.nextTick、Promise、async/awaitMutationObserver
- 宏任务(macro-task)队列
3.1 执行过程:
Event Loop 也可以理解为:不断地从**任务队列中取出任务到调用栈**执行的一个过程。
- 执行并出队一个
**macro-task**。- 初始状态时:
调用栈空,micro 队列空,macro 队列里有且只有一个script脚本(整体代码)。这时首先执行并出队的就是 script 脚本;
- 初始状态时:
- 全局上下文(script 标签)被推入调用栈,同步代码执行。
- 在执行的过程中,通过对一些接口(
**WebAPIs**)的调用,可以产生新的macro-task与micro-task,它们会分别被推入各自的任务队列里。
- 在执行的过程中,通过对一些接口(
这个过程本质上是:队列的 macro-task 的执行和出队的过程;
注意:JavaScript 的代码执行时,同步任务会被依次加入执行栈中先执行;
而对于异步任务,其是在拿到结果的时候,才将注册的**回调函数**放入任务队列,这一步由**事件触**``**发线程**进行监听,而不是说注册一个异步任务就会被放在这个任务队列中。
上面操作中,出队了一个
macro-task,这一步出队micro-task,但要注意:
macro-task出队时,任务是一个一个执行的;micro-task出队时,任务是一队一队执行的(如下图所示)。
因此,处理 micro 队列时,会逐个执行队列中的任务并把它出队,直到队列被清空;
- 执行渲染操作,更新界面;
- 检查是否存在 Web worker 任务,如果有,则对其进行处理。
3.2 宏任务和微任务
任务队列是一个消息队列,先进先出,后来的事件都是被加在队尾,等到前面的事件执行完了才会被执行。而如果在执行的过程中突然有重要的任务是无法得到及时处理的。
因此就产生了宏任务和微任务,微任务使得一些异步任务得到及时的处理。
形象点来说:
- 去营业厅办业务时会需要排队,当叫到你的号码的时,就去窗口办充值业务(宏任务执行),
- 在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不会让你重新排队。
- 宏任务的真面目是浏览器派发,与 JS 引擎无关的,参与了 Event Loop 调度的任务
- 微任务是在运行宏任务/同步任务的时候产生的,是属于当前任务的,所以它不需要浏览器的支持,不会经过Web APIs,内置在 JS 当中,直接在 JS 的引擎中就被执行掉了。
3.3 总述:
每次的执行过程,其实都是一个宏观任务,我们可以大概理解:宏观任务的队列就相当于事件循环。
在宏观任务中,JavaScript 的 Promise等还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。
3.4 回调补充
**
