- 作者:老汪软件技巧
- 发表时间:2024-11-07 17:01
- 浏览量:59
什么是事件循环
在了解事件循环前,我们需要一些 JavaScript 的基本特性知识。
JavaScript 是单线程的,这意味着在同一时间只能执行一个任务。而像 Java 这样的多线程语言,可以同时进行多个任务的处理。JavaScript 的任务主要分为同步任务和异步任务。异步任务是那些不连续执行的任务,比如网络请求、文件读取等,这些任务可能花费较长的时间,但并不需要 JavaScript 引擎的持续工作。在这种情况下,JavaScript 引擎可以将这些任务交给外部系统(例如浏览器或操作系统),待这些任务准备好后,再执行相应的回调函数;同步任务则是直接顺序执行的。
如果没有特殊的处理方式,JavaScript 在执行异步任务时将会等待任务完成再继续执行,这样会造成大量时间的浪费,尤其是在单线程环境下,JavaScript 没有额外的线程去执行其他任务,只能“忙等”。
为了避免这种等待,JavaScript 采用了“事件循环”机制,允许在异步任务未完成时继续执行其他同步任务,等到异步任务完成,再执行它们的回调函数。这种模式实现了非阻塞式的运行,大大提升了 JavaScript 的执行效率。
事件循环通过一个任务队列管理异步任务的回调,这个队列是先进先出的。根据任务的类型,队列分为“宏任务队列”和“微任务队列”,每当 JavaScript 引擎空闲时,就会依次处理这些队列中的任务,从而实现循环处理。这种机制也被称为“事件循环”。
宏任务和微任务
事件循环是由宏任务(macrotask)和微任务(microtask)组成的机制,其中JavaScript引擎按顺序从宏任务队列中取出一个任务执行,每执行完一个宏任务后,会处理所有微任务队列中的任务,直到微任务队列清空,然后继续下一个宏任务,如此循环往复。
宏任务 (Macrotasks)微任务 (Microtasks)浏览器的事件循环
在浏览器环境中,事件循环的工作机制比较简单。它有两个主要的任务队列:
在浏览器中,事件循环的流程如下:
执行当前的同步任务(即当前脚本中的代码)。执行微任务队列中的所有任务。渲染更新(例如重新绘制页面,浏览器会在这一阶段进行优化,尽量减少多次渲染)。执行宏任务队列中的任务。重复上述步骤,直到队列为空。
事件循环示例(浏览器环境):
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('Script end');
输出:
在这个例子中,Promise 回调会在 setTimeout 之前执行,因为微任务队列的优先级高于宏任务队列。
Node.js 环境下的事件循环
Node.js 的事件循环则更加复杂,因为它需要处理异步 I/O 操作,并且有更多的队列和阶段来处理不同类型的任务。Node.js 的事件循环有六个主要的阶段:
timers:处理 setTimeout 和 setInterval 等定时器回调。I/O callbacks:执行一些延迟的 I/O 回调(如 fs.readFile 的回调)。idle, prepare:内部使用,通常没有任务。poll:检查和处理新的 I/O 事件,执行 I/O 回调。check:执行 setImmediate 的回调。close callbacks:处理关闭回调(例如 socket.on('close', ...))。
Node.js 在每个阶段都有可能执行任务。一个任务队列(例如 setTimeout 的回调)会在 timers 阶段执行,而与 I/O 操作相关的任务则会在 poll 阶段执行。setImmediate 的回调则会在 check 阶段执行。
事件循环示例(Node.js 环境):
process.nextTick(() => {
console.log('process.nextTick callback');
});
// 创建一个 Promise 作为微任务
Promise.resolve().then(() => {
console.log('Promise callback');
});
// 使用 setTimeout 作为宏任务
setTimeout(() => {
console.log('setTimeout callback');
// 在 setTimeout 回调内创建另一个微任务
Promise.resolve().then(() => {
console.log('Promise callback inside setTimeout');
});
}, 300);
// 主线程上的同步代码
console.log('Main thread execution');
输出:
首先打印的是主线程上的同步代码'Main thread execution'。接着,由于process.nextTick会在当前操作完成后立即执行,所以'process.nextTick callback'会在下一个阶段执行。然后,微任务队列中的Promise回调被执行,打印出'Promise callback'。setTimeout是一个宏任务,它会在指定的时间间隔(这里是300毫秒)之后被执行。一旦这个定时器到期,事件循环会将setTimeout的回调放入宏任务队列中。当setTimeout的回调被执行时,它会打印'setTimeout callback',并且在该回调中又创建了一个新的微任务Promise。最后,在setTimeout回调执行完毕后,事件循环会再次检查微任务队列,并执行其中的Promise回调,打印出'Promise callback inside setTimeout'。关键区别总结特性浏览器环境Node.js 环境
事件循环的阶段
主要有宏任务队列和微任务队列,渲染阶段
6 个阶段(timers、I/O callbacks、poll、check、close callbacks)
宏任务
包括 setTimeout、setInterval、UI 渲染等
包括 setTimeout、setInterval、I/O 操作等
微任务
Promise、MutationObserver 等
Promise、process.nextTick 等
执行顺序
微任务优先于宏任务执行
微任务优先于宏任务执行,但有额外阶段(如 check)
处理 I/O 操作
基于浏览器事件驱动,不涉及文件操作
强调 I/O 操作,通过底层 libuv 库支持非阻塞 I/O