ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

js和node事件循环区别

js和node事件循环区别

区别:

两边都是“宏任务 + 微任务”,但 Node 把宏任务拆成了6 个阶段按固定顺序跑,微任务不在每个宏任务后清,而是在阶段切换间隙清空;另外 Node 多了process.nextTick,优先级比 Promise 微任务还高。

细节版

维度

浏览器

Node.js

宏任务来源

setTimeout/setInterval/MessageChannel/requestAnimationFrame

分6 阶段:timers(setInterval,setTimeout) → pending callbacks → idle/prepare →poll​ → check(setImmediate) → close callbacks

微任务时机

每个宏任务结束后立刻清空

每个阶段之间清空微任务;poll 空闲才去 check 跑setImmediate

最高优先级

无(微任务已是最前)

process.nextTick队列,先于所有微任务​

setTimeoutvssetImmediate

无setImmediate

poll 之后执行setImmediate;setTimeout(fn,0)在 timers 阶段跑 →I/O 回调里永远setImmediate先​

渲染

每轮末尾有 rAF → 布局 → 绘制

无 DOM 渲染

区别:

1.基于环境:
Node.js:事件循环是 Node.js 运行时的一部分,它负责管理异步操作,例如文件 I/O、网络请求等。
浏览器:事件循环是浏览器引擎的一部分,用于处理诸如 DOM 事件、Ajax 请求等浏览器特定的异步操作。


2.事件循环实现:


Node.js:Node.js 使用 libuv 库来实现事件循环。libuv 是一个跨平台的异步 I/O 库,它提供了事件循环以及线程池等功能,用于管理异步操作。
浏览器:浏览器中的事件循环由浏览器引擎实现,通常是由 Blink(Chrome 和 Opera 使用的引擎)、Gecko(Firefox 使用的引擎)或 WebKit(Safari 使用的引擎)等实现。


3.事件队列:


Node.js:Node.js 有多个事件队列,其中包括 timers 队列(setTimeout 和 setInterval 回调)、I/O 队列(处理文件 I/O、网络 I/O 等)、微任务队列(process.nextTick 和 Promise)以及 setImmediate 队列(在每个事件循环迭代结束时执行)。


浏览器:浏览器也有多个事件队列,包括宏任务队列(例如 DOM 事件、Ajax 请求等)、微任务队列(Promise 和 MutationObserver 回调)、requestAnimationFrame 队列等。


4.主线程阻塞:


Node.js:Node.js 是单线程的,当执行 CPU 密集型操作或阻塞操作时,会导致事件循环被阻塞,从而影响其他异步操作的执行。


浏览器:浏览器是多线程的,JavaScript 代码运行在主线程上,但浏览器还有其他线程,如渲染线程、网络线程等。主线程阻塞可能会导致页面渲染被延迟。

队列

归属

清空时机

process.nextTick队列

Node 特有,V8 不认识

每个原生调用栈返回后立刻清空,先于微任务

Promise.then/queueMicrotask

JS 语言标准微任务​

Node 和浏览器一致:在nextTick之后、回到事件循环前清空

setImmediate

Node 特有宏任务

check 阶段执行

setTimeout

宏任务

timers 阶段

严格说,process.nextTick既不是 JS 标准微任务,也不是宏任务——它是 Node自己实现的一个独立队列,优先级比 Promise 微任务还高,在当前操作完成后、事件循环继续往下走之前就清空。标准里(浏览器 / Web 规范)根本没有 nextTick,只有 Promise / queueMicrotask 才算“微任务”。

javaScript事件循环机制

一、是什么?

用事件循环是用来实现单线程非阻塞的,因为js是单线程语言。

JS中,所有的任务都可以分为:

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,例如ajax请求,或者setTimeout函数
  1. ​执行同步代码(调用栈中的代码,比如 console.log)​​

  2. ​​当调用栈清空后,执行当前所有已排队的微任务(如 Promise.then、queueMicrotask、MutationObserver)​​

    • ​​微任务会一次性全部执行完,直到微任务队列为空​​

  3. ​​然后,浏览器(或 Node.js)才会从宏任务队列中取出一个宏任务(如 setTimeout、setInterval、I/O、UI 渲染等)来执行​​

  4. ​​执行完一个宏任务后,再次检查是否有微任务,如果有,继续清空微任务队列​​

  5. ​​如此循环,这就是 “事件循环”​​

宏任务与微任务

✅ 1. ​​宏任务(MacroTask / Task)​​

​​宏任务是 JS 异步任务的一种,它的回调会被放入“宏任务队列”中,由事件循环在每一轮循环中按顺序取一个执行。​​

✅ 常见的宏任务包括:

宏任务

说明

setTimeout

定时器,回调进入宏任务队列

setInterval

定时器,重复执行

setImmediate(Node.js)

Node 环境的“立刻”宏任务

I/O 操作(如文件读写、网络请求)

如fetch、fs.readFile(在浏览器中一般是 Promise 化的)

UI 渲染(浏览器)

比如重绘、回流

整体 script 代码

一个<script>标签内的同步代码也视为一个宏任务


  • 回调函数:只是一种函数传递模式,本身不分同步/异步、宏任务/微任务。

  • 异步回调的分类:

    • 属于setTimeout、用户交互、I/O等宏任务源的回调 → 宏任务。

    • 属于Promise.then、queueMicrotask等微任务源的回调 → 微任务。

  • 关键区分:看回调函数被哪个异步任务包装,而非回调函数本身。

✅ 2. ​​微任务(MicroTask / Jobs)​​

​​微任务也是异步任务,但它的优先级比宏任务高,会在当前宏任务执行完成后、下一个宏任务开始之前,清空整个微任务队列。​​

✅ 常见的微任务包括:

微任务

说明

Promise.then()/Promise.catch()/Promise.finally()

Promise 的回调是微任务

MutationObserver(浏览器)

DOM 变化监听(微任务)

queueMicrotask(fn)

显式插入一个微任务

process.nextTick(Node.js,优先级比微任务还高)

Node 环境特有

执行宏任务时,遇到微任务就放入微任务任务队列中,当宏任务执行完成后,会查看微任务的事件队列,然后将微任务依次执行完,再进行循环执行下一个宏任务。

注意:await console.log(3); // ⚠️ 重点:await 的左侧是同步的!
console.log(4); // ⚠️ 这行会被“推迟”为微任务

await async2(); ⚠️ 重点:await 会让后面的代码变成“微任务”
console.log('async1 end'); //变成一个微任务了

详见下面的输出题。

node:

  • Timers:用于存储定时器的回调函数(setInterval,setTimeout)
  • Pending callbacks:执行与操作系统相关的回调函数,比如启动服务器端应用时监听端口操作的回调函数就在这里调用。
  • 闲置阶段(idle, prepare):仅系统内部使用
  • 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞
  • 检查阶段(check):setImmediate() 回调函数在这里执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,例如关闭数据库连接的回调

====================

为什么netick不算“微任务”

  • 微任务是ECMAScript 规范概念(Job Queue),浏览器和 Node 都有,只有 Promise / queueMicrotask 是。
  • process.nextTick是Nodelibuv+ process binding​ 加的钩子,浏览器压根没有(IE 那个setImmediate是另一回事)。
  • Node 官方文档自己都写:"nextTick queue is not part of the Event Loop"——它跑在事件循环每一阶段切换之前,所以能饿死 I/O。

追问防御

  • “那queueMicrotask和 Promise.then 有区别吗?”→ 优先级一样,都在 nextTick 之后;区别是queueMicrotask不会被 thenable 链式吞掉,更纯粹,Node 11+ / 浏览器通用。
  • “nextTick 和 setImmediate 谁快?”→ 同一次调用里nextTick永远先(它在阶段内就清完了);setImmediate要等事件循环走到check 阶段,哪怕只差一个阶段也是下一拍。
  • “浏览器有等价物吗?”→ 没有严格等价。queueMicrotask最接近微任务语义;以前有人用MessageChannel模拟 nextTick 的“早于 setTimeout”行为,但现在直接queueMicrotask就行。

⚠️ 红线:

  • ❌ 别说“nextTick 是 Node 的微任务”——面试官就等你踩这个坑。正确说法是“Node 独立的高优先级队列,比微任务还早,不属于规范定义的微任务”。
  • ✅ 加分收尾:“所以 Node 里实际是三层:nextTick → 微任务(Promise) → 宏任务阶段,不是大家直觉的两层。”

👉 继续。

为什么 async/await 是微任务

await console.log(3); // ⚠️ 重点:await 的左侧是同步的!

console.log(4); // ⚠️ 这行会被“推迟”为微任务

打印34

​​因为 JavaScript 的await语法规定:await的左侧表达式(例如console.log(3)或一个 Promise)是 ​​同步求值的​​,它会在暂停异步操作之前先执行并得到结果,而真正“暂停后续代码、等待 Promise 完成”的动作,才会引入异步(微任务)。​​

1.与 Promise 密切相关:async/await 是基于 Promise 实现的,await 关键字等待的是一个 Promise 对象的状态。由于 Promise 的 then 方法注册的回调函数属于微任务,因此 await 后面的代码也会作为微任务来执行。


2.顺序执行:async/await 语法让异步代码看起来像同步代码一样顺序执行,因此,await 后面的代码应该在当前任务执行结束后立即执行,以保持程序的顺序执行语义。


3.处理异步结果:await 后面的代码通常是在等待异步操作的结果,并且需要在 Promise 解决后立即执行,以便处理异步操作的结果。

async/await底层:https://blog.csdn.net/wyzqhhhh/article/details/136002372?sharetype=blogdetail&sharerId=136002372&sharerefer=PC&sharesource=wyzqhhhh&spm=1011.2480.3001.8118

为什么先微后宏

1.可以确保微任务在下一个宏任务之前执行,提高这种响应性能。这样,Promise的回调、MutationObserver等微任务能够在当前任务结束后立即执行,不必等待下一个宏任务。

  • 这对于一些需要及时响应的场景(例如Promise的resolve/reject、用户交互等)非常重要,避免了不必要的延迟。

2.而且微任务通常代表着 “本次执行流程中产生的后续逻辑”,比如:

  • Promise 的.then()是在某个异步操作完成后要立刻执行的回调

  • 它们往往依赖于当前执行上下文的数据状态,逻辑上应该尽快执行

  • 👉如果先执行宏任务,有可能导致微任务中的逻辑被延后,造成数据不一致或逻辑断裂

3.避免视觉或逻辑上的不一致(尤其是 UI 相关)

  • 在浏览器中,微任务通常在渲染之前执行

  • 如果微任务(如状态更新、Promise 回调)被延迟到宏任务之后,可能导致:

    • 数据更新了但 UI 没及时渲染

    • 用户看到 “卡顿” 或 “状态不符合预期”

👉先执行微任务,可以保证页面状态、数据变更先处理完,再考虑渲染或其它宏任务(如用户交互)

输出顺序问题

1. 用多个setTimeout解释事件循环

console.log('1'); // 同步 setTimeout(() => { console.log('2'); // 宏任务 }, 0); Promise.resolve().then(() => { console.log('3'); // 微任务 }); setTimeout(() => { console.log('4'); // 宏任务 }, 0); console.log('5'); // 同步 15324

第 1 步:执行主线程(整体 script,是一个宏任务)

  • 遇到console.log('1')➜ ​​输出 1​​

  • 遇到setTimeout(..., 0),把回调放入 ​​宏任务队列​​

  • 遇到Promise.resolve().then(...),把回调放入 ​​微任务队列​​

  • 遇到setTimeout(..., 0),​​又一个 setTimeout,回调也放入宏任务队列​​

  • 遇到console.log('5')➜ ​​输出 5​​

✅ ​​此时:主线程(第一个宏任务)执行完毕​​

​​调用栈为空,事件循环开始工作:​​


第 2 步:检查微任务队列(先执行所有微任务!)

  • 微任务队列里有:() => { console.log('3') }

  • 执行它 ➜ ​​输出 3​​

✅ ​​微任务队列清空​​


第 3 步:开始下一轮事件循环,执行第一个宏任务(setTimeout)

  • 宏任务队列中第一个是:() => { console.log('2') }

  • 执行它 ➜ ​​输出 2​​


第 4 步:检查是否有微任务(本轮没有新的微任务)

  • 没有,继续下一轮事件循环


第 5 步:执行下一个宏任务(第二个 setTimeout)

  • 宏任务队列中下一个是:() => { console.log('4') }

  • 执行它 ➜ ​​输出 4​​

console.log('1'); // 同步 setTimeout(() => { console.log('2'); // 宏任务 }, 1000); Promise.resolve().then(() => { console.log('3'); // 微任务 }); setTimeout(() => { console.log('4'); // 宏任务 }, 0); console.log('5'); // 同步 15342

“JavaScript 通过事件循环机制处理同步与异步任务。同步代码如console.log('1')和console.log('5')会立即执行;Promise.then属于微任务,在当前主线程执行完后立即执行,输出 3;而setTimeout是宏任务,即使延迟为 0 也会放入任务队列,等待下一轮事件循环执行,所以延迟 0 的setTimeout输出 4 先于延迟 1000ms 的setTimeout输出 2。最终顺序为:1 → 5 → 3 → 4 → 2。”

2.输出顺序问题

(1)

async function async1 () { console.log('async1 start'); await async2(); ⚠️ 重点:await 会让后面的代码变成“微任务” console.log('async1 end'); //变成一个微任务了 } async function async2 () { console.log('async2') }。//async2执行完事之后 看后面的同步任务。后面的同步任务走完了 再去看await后面那一条微任务!!! console.log('script start') setTimeout(function() { console.log('setTimeout') }, 0); async1(); new Promise(function (r) { console.log('promise1'); r(); }).then(function() { console.log('promise2') }); console.log('script end'); script start async1 start async2 promise script end async1 end promise2 setTimeout

(2)

function async2() {
console.log('async2');
return Promise.resolve(); // 返回一个 resolved 的 Promise
}

function async1() {
console.log('async1 start');
return async2().then(() => {
console.log('async1 end'); // 这是微任务
});
}

async1();

打印:

async1 start
async2
async1 end


(3)

console.log(1); async function async1() { console.log(2); await console.log(3); // ⚠️ 重点:await 的左侧是同步的! console.log(4); // ⚠️ 这行会被“推迟”为微任务 } setTimeout(function () { console.log(5) }) async1() new Promise(function (resolve) { console.log(6); resolve() }).then(function () { console.log(7) }) console.log(8) 12368475

​​因为 JavaScript 的await语法规定:await的左侧表达式(例如console.log(3)或一个 Promise)是 ​​同步求值的​​,它会在暂停异步操作之前先执行并得到结果,而真正“暂停后续代码、等待 Promise 完成”的动作,才会引入异步(微任务)。​​

(4)

执行 console.log('7. setTimeout 2');→ 输出 '7. setTimeout 2' 然后执行:Promise.resolve().then(...)→ ​​这个是一个微任务!​​ ⚠️ ​​但注意!这个 Promise.then 是在宏任务内部创建的,它不会立即执行,而是被放入当前宏任务执行完后的微任务队列中!​​ ​​当前宏任务(即 setTimeout 的回调)执行完毕​​ ​​事件循环机制会清空当前微任务队列​​ 这时候,刚刚那个 Promise.resolve().then(...)里的回调会被执行: console.log('1. 开始'); setTimeout(() => { console.log('2. setTimeout'); }, 0); Promise.resolve().then(() => { console.log('3. Promise 1'); }); console.log('4. 中间'); Promise.resolve().then(() => { console.log('5. Promise 2'); Promise.resolve().then(() => { console.log('6. 内层 Promise'); }); }); setTimeout(() => { console.log('7. setTimeout 2'); Promise.resolve().then(() => { console.log('8. setTimeout 中的 Promise'); }); }, 0); console.log('9. 结束'); 149356278
  1. ​执行同步代码(调用栈中的代码,比如 console.log)​​

  2. ​​当调用栈清空后,执行当前所有已排队的微任务(如 Promise.then、queueMicrotask、MutationObserver)​​

    • ​​微任务会一次性全部执行完,直到微任务队列为空​​

  3. ​​然后,浏览器(或 Node.js)才会从宏任务队列中取出一个宏任务(如 setTimeout、setInterval、I/O、UI 渲染等)来执行​​

  4. ​​执行完一个宏任务后,再次检查是否有微任务,如果有,继续清空微任务队列​​

  5. ​​如此循环,这就是 “事件循环”​​

(6)

task1() // 同步任务(执行 500ms) const timer1 = setTimeout(cb1,100)// cb1 是宏任务回调 const timer2 = setTimeout(cb2, 200) const timer3 = setTimeout(cb3,600) // 在task1开始的第400ms,用户发起点击,对应clickCb const clickcb = function () { // 点击事件回调,执行时长200ms 点击事件的回调是宏任务,用户交互属于宏任务 } ✅ 关键结论 点击事件的触发不是“立即执行回调”,而是“将回调加入队列,等待主线程调度”。 能否立即执行取决于主线程是否空闲:若主线程被长任务(如 task1)占用,点击回调会被延迟执行。 实际开发中的优化:避免长任务阻塞主线程(如拆分任务、用 Web Worker),以提升交互响应速度。 • 定时器延迟是“相对主线程空闲时间”:即使定时器设置了延迟,若主线程被长任务阻塞,回调会延迟到主线程空闲后才执行。 • 用户事件会被“延迟执行”:若主线程被占用,用户事件的回调会排队等待,直到主线程空闲。 • 定时器入队时间决定了执行顺序:先入队的定时器(如 timer1)会优先执行,即使它的延迟时间更短。 ✅ 最终执行时间表(相对 task1 启动时刻) 回调函数 入队时间 实际执行开始时间 执行耗时 说明 timer1 100ms 500ms 0ms 主线程空闲后立即执行 timer2 200ms 600ms 0ms 主线程空闲后立即执行 clickCb 400ms 700ms 200ms 被 task1 阻塞后执行 timer3 600ms 1100ms 0ms 入队时间(600ms)早于执行时间 ✅ 时间线推演(单位:毫秒,从 task1 开始计时) 1. task1 执行(持续 500ms) • 时间段:0ms ~ 500ms • 行为: • 主线程被 task1 占用,无法执行其他任务(如定时器回调、点击事件)。 • 定时器 timer1(100ms)、timer2(200ms)、timer3(600ms)的计时器已启动,但回调不会立即执行,需等待主线程空闲。 2. 用户点击事件触发(第 400ms) • 触发时刻:400ms(task1 执行到第 400ms 时) • 行为: • 点击事件的回调 clickCb(执行时长 200ms)会被立即加入宏任务队列,但由于主线程正忙于 task1,需等待 task1 完成后才执行。 3. task1 完成(500ms 时刻) • 完成时刻:500ms • 行为: • 主线程释放,开始处理已入队的宏任务(按入队顺序执行)。 • 此时,宏任务队列中的任务按入队时间排序: 1. timer1 回调(入队时间:100ms) 2. timer2 回调(入队时间:200ms) 3. clickCb(入队时间:400ms) 4. timer3 回调(入队时间:600ms,暂未触发) 4. 宏任务队列执行顺序 时间段 执行任务 任务入队时间 任务执行耗时 实际执行开始时间 500ms~600ms timer1回调 100ms 立即完成 500ms 600ms~700ms timer2 回调 200ms 立即完成 600ms 700ms~900ms clickCb 400ms 200ms 700ms 1100ms~1200ms timer3 回调 600ms 立即完成 1100ms

(7)

setTimeout(function () { console.log("1") }) new Promise(function (resolve) { console.log("2"); resolve(); console.log("3"); }).then(function () { console.log("4") }) //2341 // ​​resolve()是 Promise 构造函数中用来将 Promise 的状态从 ​​pending(等待中)​​ 变为 ​​fulfilled(已兑现)​​ 的方法。只有调用了 resolve()(或 reject()),.then()或 .catch()中的回调才会被放入任务队列,并在适当的时候执行。如果不调用 resolve(),Promise 会一直处于 pending 状态,.then()回调永远不会执行。​​
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进