区别:
两边都是“宏任务 + 微任务”,但 Node 把宏任务拆成了6 个阶段按固定顺序跑,微任务不在每个宏任务后清,而是在阶段切换间隙清空;另外 Node 多了
process.nextTick,优先级比 Promise 微任务还高。
细节版
维度 | 浏览器 | Node.js |
|---|---|---|
宏任务来源 |
| 分6 阶段:timers(setInterval,setTimeout) → pending callbacks → idle/prepare →poll → check( |
微任务时机 | 每个宏任务结束后立刻清空 | 每个阶段之间清空微任务;poll 空闲才去 check 跑 |
最高优先级 | 无(微任务已是最前) |
|
| 无 | poll 之后执行 |
渲染 | 每轮末尾有 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 代码运行在主线程上,但浏览器还有其他线程,如渲染线程、网络线程等。主线程阻塞可能会导致页面渲染被延迟。
队列 | 归属 | 清空时机 |
|---|---|---|
| Node 特有,V8 不认识 | 每个原生调用栈返回后立刻清空,先于微任务 |
Promise | JS 语言标准微任务 | Node 和浏览器一致:在 |
| Node 特有宏任务 | check 阶段执行 |
| 宏任务 | timers 阶段 |
严格说,process.nextTick既不是 JS 标准微任务,也不是宏任务——它是 Node自己实现的一个独立队列,优先级比 Promise 微任务还高,在当前操作完成后、事件循环继续往下走之前就清空。标准里(浏览器 / Web 规范)根本没有 nextTick,只有 Promise / queueMicrotask 才算“微任务”。
javaScript事件循环机制
一、是什么?
用事件循环是用来实现单线程非阻塞的,因为js是单线程语言。
JS中,所有的任务都可以分为:
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,例如ajax请求,或者setTimeout函数
执行同步代码(调用栈中的代码,比如 console.log)
当调用栈清空后,执行当前所有已排队的微任务(如 Promise.then、queueMicrotask、MutationObserver)
微任务会一次性全部执行完,直到微任务队列为空
然后,浏览器(或 Node.js)才会从宏任务队列中取出一个宏任务(如 setTimeout、setInterval、I/O、UI 渲染等)来执行
执行完一个宏任务后,再次检查是否有微任务,如果有,继续清空微任务队列
如此循环,这就是 “事件循环”
宏任务与微任务
✅ 1. 宏任务(MacroTask / Task)
宏任务是 JS 异步任务的一种,它的回调会被放入“宏任务队列”中,由事件循环在每一轮循环中按顺序取一个执行。
✅ 常见的宏任务包括:
宏任务 | 说明 |
|---|---|
| 定时器,回调进入宏任务队列 |
| 定时器,重复执行 |
| Node 环境的“立刻”宏任务 |
I/O 操作(如文件读写、网络请求) | 如 |
UI 渲染(浏览器) | 比如重绘、回流 |
整体 script 代码 | 一个 |
回调函数:只是一种函数传递模式,本身不分同步/异步、宏任务/微任务。
异步回调的分类:
属于
setTimeout、用户交互、I/O等宏任务源的回调 → 宏任务。属于
Promise.then、queueMicrotask等微任务源的回调 → 微任务。
关键区分:看回调函数被哪个异步任务包装,而非回调函数本身。
✅ 2. 微任务(MicroTask / Jobs)
微任务也是异步任务,但它的优先级比宏任务高,会在当前宏任务执行完成后、下一个宏任务开始之前,清空整个微任务队列。
✅ 常见的微任务包括:
微任务 | 说明 |
|---|---|
| Promise 的回调是微任务 |
| DOM 变化监听(微任务) |
| 显式插入一个微任务 |
| 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
执行同步代码(调用栈中的代码,比如 console.log)
当调用栈清空后,执行当前所有已排队的微任务(如 Promise.then、queueMicrotask、MutationObserver)
微任务会一次性全部执行完,直到微任务队列为空
然后,浏览器(或 Node.js)才会从宏任务队列中取出一个宏任务(如 setTimeout、setInterval、I/O、UI 渲染等)来执行
执行完一个宏任务后,再次检查是否有微任务,如果有,继续清空微任务队列
如此循环,这就是 “事件循环”
(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()回调永远不会执行。