【TypeScript】事件循环和LibUV简述
文章目录
- 一、事件循环
- 1、Node.js 事件循环
- 1.1 基本概念
- 1.2 事件循环阶段
- 1.3 事件循环流程
- 1.4 宏任务与微任务
- 2、浏览器事件循环
- 2.1 基本概念
- 2.2 宏任务和微任务
- 二、LibUV库
- 1、LibUV 与 Node.js 事件循环的关系
一、事件循环
JS 或 TS 是单线程,这意味着它只有一个主线程(执行栈)来处理所有任务。这种设计避免了多线程环境中的复杂同步问题,但如何防止长时间运行的代码产生阻塞。
解决方案是将代码分为:
同步代码:由 JS 引擎直接执行
异步代码:交给宿主环境(浏览器/Node.js)处理
1、Node.js 事件循环
JS 引擎本身不实现事件循环机制,这是由它的宿主实现的,浏览器中的事件循环主要是由浏览器来实现,而在 NodeJS 中也有自己的事件循环实现。NodeJS 中也是循环 + 任务队列的流程以及微任务优先于宏任务,大致表现和浏览器是一致的。
1.1 基本概念
事件循环是 Node.js 处理非阻塞 I/O 的核心机制,使得单线程可以高效处理多个并发请求。
Node.js 是基于单线程的 JS 运行时,利用事件循环处理异步操作。
NodeJS 的事件循环机制都是基于 Libuv 库实现的。NodeJS 中 V8 引擎将 JS 代码解析后调用 Node API,然后 Node API 将任务交给 Libuv 去分配,最后再将执行结果返回给 V8 引擎。在 Libux 中实现了一套事件循环流程来管理这些任务的执行,所以 NodeJS 的事件循环主要是在 Libuv 中完成的。
1.2 事件循环阶段
- Timers:执行所有
setTimeout()和setInterval()回调。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。 - I/O Callbacks:处理一些延迟的回调。
- Idle,prepare:内部使用。
- Poll(轮询):检索新的 I/O 事件,执行 I/O 相关回调。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。
- Check:执行
setTmmediate()回调。 - Close Callbacks:处理关闭回调。
每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。
setTimeout(() => {console.log('timeout1')Promise.resolve().then(function() {console.log('promise1')})
}, 0);
setTimeout(() => {console.log('timeout2')Promise.resolve().then(function() {console.log('promise2')})
}, 0);
- 浏览器中运行
每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。
- NodeJS 中运行
因为输出 timeout1 时,当前正处于 timers 阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。
NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。
1.3 事件循环流程
- 任务进入事件循环队列
- 事件循环按顺序执行。
- 在 Poll 阶段等待新的时间到达,如果没有,检查其他阶段的回调。
- 如果
setImmediate()和setTimeout()都存在,setImmediate()在check阶段先执行,而setTimeout()在timers阶段执行。
setTimeout(() => {console.log('Timeout callback');
}, 0);setImmediate(() => {console.log('Immediate callback');
});console.log('Main thread execution');// 输出顺序:
// Main thread execution 先打印。
// setImmediate() 和 setTimeout() 的执行顺序取决于当前事件循环的状态,一般 setImmediate() 会先执行。
1.4 宏任务与微任务
- 宏任务:
setTimeout、setInterval、setImmediate、I/O 操作等。 - 微任务:
process.nextTick、Promise.then。
**执行顺序:**微任务优先级高于宏任务,会在当前阶段的回调结束后立即执行。
2、浏览器事件循环

2.1 基本概念
TS 或 JS 执行代码时,会将同步代码按遍历顺序存放到执行栈中,然后依次执行,遇到异步任务就交给其他线程,异步执行结束会通知主线程,事件循环会取出已完成的异步任务回调,放到主线程(即执行栈)中去执行。期间再次遇到异步则同理。
JS 按顺序执行执行栈中的方法,每次执行一个方法时,会为这个方法生成独有的执行环境,待这个方法执行完成后,销毁当前的执行环境,并从栈中弹出此方法,然后继续下一个方法。
所以通过至少一次循环检测任务队列中是否有新的任务,然后不断取出异步回调去执行,这个过程就是浏览器的事件循环。
2.2 宏任务和微任务
- 宏任务:
setTimeout()、setInterval()、setImmediate()、I/O 操作等。 - 微任务:
process.nextTick()、Promise.then()/.catch()。
console.log('1');
setTimeout(() => {console.log('setTimeout')
}, 0)
// new Promise 是同步的,promise.then 是异步的。
new Promise((resolve) => {console.log('2')resolve()
}).then(() => {console.log('promise.then')
})
console.log('3');// 最终输出"1"、"2"、"3"、"promise.then"、"setTimeout"
在执行 setTimeout() 时,浏览器会将任务给到浏览器的定时器线程去执行,计时结束,才放回任务队列等待主线程取出。如果这时主线程还在执行同步任务的过程中,那么此时的宏任务就只有先挂起,这就造成了计时器不准确的问题。同步代码耗时越长,计时器的误差就越大。
而在执行Promise.then() 时,V8 引擎不会将异步任务交给浏览器其他线程,而是将回调存在自己的一个队列中,待当前执行栈执行完成后,立马去执行 promise.then 存放的队列,promise.then 微任务没有多线程参与。
宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。
微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持。

异步任务的返回结果会被放到一个任务队列中,根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中去。
在当前执行栈为空时,主线程会查看微任务队列是否有事件存在
- 存在,依次执行队列中的事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的事件,把当前的回调加到当前指向栈。
- 如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;
当前执行栈执行完毕后时会立刻处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
二、LibUV库
libuv 是跨平台支持库,最初是为 Node.js 编写的。它的设计 围绕事件驱动的异步 I/O 模型。
libuv 为用户提供了 2 个抽象,结合事件循环: 句柄和请求。
-
句柄表示能够在活动时执行某些作的长期对象。一些例子:
-
准备句柄在活动时每次循环迭代都会调用一次回调。
-
每次有新连接时都会调用其连接回调的 TCP 服务器句柄。
-
-
请求表示(通常)短期。可以通过 handle:写请求用于在句柄上写入数据;或独立:getaddrinfo 请求 不需要句柄,它们直接在循环上运行。
libuv的主要特点包括:
- 异步 I/O 管理
- 处理文件 I/O(fs 模块)、网络 I/O(net、http 模块)、管道 I/O 等操作。
- 采用 “非阻塞 I/O + 事件通知” 模式,避免主线程阻塞。
- 事件循环实现
- 定义 Node.js 事件循环的 6 个阶段(timers、pending callbacks 等),按顺序调度任务。
- 每个阶段对应特定类型的回调队列,确保任务有序执行。
- 定时器与延迟操作
- 实现 setTimeout、setInterval 的底层逻辑,管理定时器回调的触发时机。
- 提供 setImmediate 专属支持,与定时器形成差异化调度。
libuv的使用通常涉及以下几个步骤:
初始化:使用uv_loop_init初始化事件循环。
创建句柄:根据需要创建相应的句柄,如TCP句柄、UDP句柄等。
启动事件循环:使用uv_run启动事件循环。
关闭句柄:在不再需要句柄时,使用uv_close关闭句柄。
清理资源:在程序结束时,使用uv_loop_close清理事件循环。
1、LibUV 与 Node.js 事件循环的关系
- LibUV 是 Node.js 事件循环的 “实现者”,Node.js 的事件循环本质就是 LibUV 的事件循环。
- V8 引擎负责解析执行 JavaScript 代码,遇到异步操作(如 fs.readFile)时,会委托给 LibUV 处理。
- LibUV 完成底层 I/O 操作后,将回调函数放入对应阶段的队列,等待事件循环调度执行。
