实现
JavaScript 事件循环机制以运行时环境划分,可分为浏览器、Node.js及Deno事件循环机制,三者的实现技术不一样:
- 浏览器 Event Loop 是 HTML 中定义的规范;
- Node.js Event Loop 是由 libuv 库实现;
- Deno Event Loop 是 由 Rust 语言的 Tokio 库实现。
这里只讲浏览器部分。
进程、线程
进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。
浏览器进程
浏览器模式一般有两种,单进程架构和多进程多线程架构。现代浏览器基本上都是后者了。
因为现在网页复杂性非常高,如果整个浏览器是单进程的,有可能某个page界面的抛错就会导致整个浏览器的崩溃。同时多个界面互相可以访问相同的内存和相同的执行环境,安全性也是一个大的问题,所以浏览器需要采用多进程模式。
以chrome为例,浏览器的进程大概分为以下这几种:
- 浏览器主进程(Browser进程):控制chrome的地址栏,书签栏,返回和前进按钮,同时还有浏览器的不可见部分,例如网络请求和文件访问。
- 第三方插件进程:每种插件一个进程,插件运行时才会创建。
- 浏览器渲染进程(也被称为浏览器内核,内部是多线程的):负责界面渲染,脚本执行,事件处理等。
- GPU进程:最多一个,用于3D绘制。
- 工具进程:比如我们调试的控制台。
浏览器内核
浏览器是多进程的,浏览器每一个 tab 标签一般代表一个独立的进程(多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。
浏览器内核有多种线程在工作。
GUI 渲染线程
负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。
JS 引擎线程
单线程工作,负责解析运行 JavaScript 脚本。它一般就是我们常说的主线程。
和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。
事件触发线程
当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。
定时器触发线程
浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。
http 请求线程
http 请求的时候会开启一条请求线程。
请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。
从上面可以看出,JS 引擎线程外的其它线程的后续工作都会添加到任务队列中。
而JS引擎线程是单线程,每次只能执行一项任务,其它任务都得按照顺序排队等待被执行,只有当前的任务执行完成之后才会往下执行下一个任务。
同步任务、异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。
同步任务会在调用栈中按照顺序排队等待主线程执行;
异步任务则会在异步有了结果后,将注册的回调函数添加到任务队列中,等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。
Event Loop
调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。
每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环(event loop)。
宏任务(macro-task)、微任务(micro-task)
除了广义的同步任务和异步任务,JavaScript 单线程中的任务还可以细分为宏任务和微任务。
macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O等。
micro-task包括:process.nextTick, Promise, Object.observe(已废弃), MutationObserver。
宏任务本质,是参与了事件循环的任务,就是上面图上的GUI渲染线程、事件触发线程、定时器触发线程、http请求线程传递过来的消息。
微任务本质,是直接在 Javascript 引擎中执行的,没有参与事件循环的任务。它是V8的一个实现,本质上它和当前的V8调用栈是同步执行的,只是放到了最后面。类似于hooks,像是一个后门,专门在宏任务执行完成后,执行所有微任务,执行完成后,才会进行下一轮宏任务。