核心内容:Event Loop设计理念;微任务的作用。
1. 背景
同步I/O、异步I/O
- 同步I/O
- 进程向操作系统发起I/O;操作系统将进程挂起,开始执行I/O操作;等I/O操作完成,操作系统将数据放到进程可以访问的地方,重新激活进程;进程开始继续执行。
- 特点:在进行I/O操作期间,进程是停止执行的,被I/O操作阻塞了。
- 异步I/O
- 进程注册I/O操作回调,向操作系统发起I/O操作,然后进程继续;操作开始执行I/O操作;等I/O操作完成以后,操作系统在合适的时机让进程从回调函数处开始执行。
- 特点:进程不会被I/O操作阻塞。
2. Event Loop的目的
JavaScript 主要是单线程的,这降低了并发编程的复杂度。
同时,JavaScript 需要处理各种潜在的耗时操作(如等待用户交互、发起网络请求等)。
为了保持高效性和响应性,JavaScript 采用了异步 I/O 模型。 为了协调 JavaScript 的执行和这些异步操作,需要一个特殊的组件,这就是 Event Loop。JavaScript 负责发起异步操作,而 Event Loop 负责管理这些操作的执行和结果的处理,在适当的时机通知 JavaScript 执行相应的回调函数或解析 Promise。
3. Event Loop的基本模型
- 维护宏任务队列、微任务队列。
- 队列接收来自各方提交的任务。
- 循环执行任务队列中的回调函数。
- 当空闲的时候,先执行微任务队列里面所有任务,然后执行一个宏任务,循环循环。
4. 微任务的作用
更细粒度的执行控制
5. 浏览器的Event Loop
- 宏任务:settimeout的回调,ajax的回调等
- 微任务:Promise的回调等、MutationObserver。
- 更细粒度的执行控制:微任务允许在当前执行上下文结束后立即执行某些操作,而不需要等待整个宏任务队列。
- 避免不必要的UI更新:在浏览器环境中,UI 更新通常发生在每个宏任务之后。使用微任务可以在 UI 更新之前完成相关操作,potentially 减少不必要的重绘。
- 保证执行顺序:微任务可以确保某些相关的异步操作按预期顺序执行,而不被其他宏任务打断。
- 提高响应速度:对于需要快速响应的操作(如 Promise 链),使用微任务可以确保它们在下一个渲染帧之前执行,提供更流畅的用户体验。
- 优化资源使用:微任务可以更有效地利用每个宏任务周期,在进入下一个宏任务之前处理相关的小任务。
- 实现循环的执行流程:
- 执行任务栈到结束;
- 检测微任务队列,如果有则全部执行;
- 检测宏任务队列,执行一个;
- 如果需要,更新渲染
- 循环这个过程
6. Node的Event Loop
-
Node的Event Loop阶段更多,控制更加精细。
-
六个主要阶段:
- timers:执行 setTimeout 和 setInterval 的回调
- pending callbacks:执行延迟到下一个循环迭代的 I/O 回调
- idle, prepare:仅系统内部使用
- poll:检索新的 I/O 事件;执行 I/O 相关的回调
- check:执行 setImmediate() 的回调
- close callbacks:执行关闭的回调函数,如 socket.on('close', ...) Event Loop 会不断循环这些阶段,直到没有更多的异步操作需要处理。
-
微任务处理:
- process.nextTick 的回调在每个阶段之间执行,优先级最高
- Promise 的回调和其他微任务在每个阶段执行完后执行
7. libuv
-
libuv 提供了 Node.js 事件循环的基础实现,而 Node.js 在此基础上构建了自己的事件循环机制。
-
Node.js 的事件循环包含了 libuv 的核心功能,但同时也添加了特定于 Node.js 的抽象、阶段和功能。
-
核心功能:
- 跨平台支持
- 事件循环:libuv实现了一个事件驱动的编程模型,核心是其事件循环。
- 异步 I/O 操作:文件系统操作、网络操作(TCP, UDP)、DNS 操作。
- 线程池:libuv 维护了一个线程池,用于执行不能异步完成的阻塞操作(如某些文件系统操作)。
- 信号处理:提供了处理系统信号的机制。
- 定时器:实现了高精度的定时器功能。
-
工作流程
- 任务提交:Node.js通过 libuv 的 API 提交异步操作请求。
- 事件循环处理:libuv 的事件循环不断检查是否有完成的 I/O 操作、定时器到期等事件。
- 回调执行:当操作完成时,libuv 将相应的回调函数加入到事件循环中,在适当的时机执行。
- 继续循环:事件循环继续运行,处理新的事件和回调。
-
对非阻塞I/O的处理:对于可以非阻塞完成的操作(如网络 I/O),libuv 使用操作系统提供的非阻塞机制(如 epoll、kqueue、IOCP)。
-
线程池处理:对于无法异步完成的操作(如某些文件系统操作),libuv 将任务提交到其内部的线程池。