1.29 - Event Loop、宏任务与微任务
JavaScript Event Loop、宏任务与微任务深度解析
一、为什么需要 Event Loop?
1.1 JavaScript 的单线程特性
JavaScript 是单线程语言,这意味着同一时间只能执行一个任务。但这带来了一个问题:
// 如果没有异步机制,这段代码会阻塞后续所有代码
const data = fetch('https://api.example.com/data'); // 假设需要3秒
console.log('这行代码要等3秒才能执行');
为了解决这个问题,JavaScript 引入了异步机制和 Event Loop(事件循环)。
1.2 运行时环境的组成
JavaScript 运行时由以下几个部分组成:
- 调用栈(Call Stack): 执行同步代码
- Web APIs: 浏览器提供的异步API(setTimeout、DOM事件、fetch等)
- 任务队列(Task Queue): 存放待执行的回调函数
- 微任务队列(Microtask Queue): 存放优先级更高的回调
- Event Loop: 协调者,负责调度执行
二、Event Loop 运行机制
2.1 核心执行流程
1. 执行同步代码(调用栈中的代码)
2. 调用栈清空后,检查微任务队列
3. 执行所有微任务(直到微任务队列清空)
4. 执行一个宏任务
5. 再次检查微任务队列,执行所有微任务
6. 渲染更新(如果需要)
7. 回到步骤4,循环往复
2.2 图解 Event Loop
┌───────────────────────────┐
│ Call Stack (调用栈) │
│ 执行同步代码 │
└───────────┬───────────────┘
│
↓
┌───────────────────────────┐
│ Microtask Queue │
│ (微任务队列) │
│ - Promise.then │
│ - MutationObserver │
│ - queueMicrotask │
└───────────┬───────────────┘
│
↓
┌───────────────────────────┐
│ Macrotask Queue │
│ (宏任务队列) │
│ - setTimeout │
│ - setInterval │
│ - setImmediate(Node) │
│ - I/O │
│ - UI rendering │
└───────────────────────────┘
三、宏任务(Macrotask)
3.1 什么是宏任务?
宏任务是由宿主环境(浏览器/Node.js)发起的任务,每次 Event Loop 只执行一个宏任务。
3.2 常见的宏任务
// 1. setTimeout
setTimeout(() => {
console.log('宏任务: setTimeout');
}, 0);
// 2. setInterval
const intervalId = setInterval(() => {
console.log('宏任务: setInterval');
}, 1000);
// 3. setImmediate (Node.js 环境)
setImmediate(() => {
console.log('宏任务: setImmediate');
});
// 4. I/O 操作
fs.readFile('./file.txt', () => {
console.log('宏任务: 文件读取完成');
});
// 5. UI rendering (浏览器环境)
requestAnimationFrame(() => {
console.log('宏任务: 页面渲染');
});
// 6. MessageChannel
const channel = new MessageChannel();
channel.port1.onmessage = () => {
console.log('宏任务: MessageChannel');
};
channel.port2.postMessage('');
3.3 宏任务的特点
- 每次 Event Loop 只取出一个宏任务执行
- 执行完一个宏任务后,会清空所有微任务
- setTimeout(fn, 0) 并不是立即执行,而是在下一轮 Event Loop 中执行
四、微任务(Microtask)
4.1 什么是微任务?
微任务是由 JavaScript 引擎发起的任务,在当前宏任务执行完后立即执行,会清空整个微任务队列。
4.2 常见的微任务
// 1. Promise.then/catch/finally
Promise.resolve().then(() => {
console.log('微任务: Promise.then');
});
// 2. async/await (本质是 Promise)
async function asyncFunc() {
await Promise.resolve();
console.log('微任务: async/await');
}
asyncFunc();
// 3. queueMicrotask
queueMicrotask(() => {
console.log('微任务: queueMicrotask');
});
// 4. MutationObserver
const observer = new MutationObserver(() => {
console.log('微任务: MutationObserver');
});
observer.observe(document.body, { attributes: true });
document.body.setAttribute('data-test', 'value');
// 5. process.nextTick (Node.js,优先级最高)
process.nextTick(() => {
console.log('微任务: process.nextTick');
});
4.3 微任务的特点
- 在当前宏任务执行完后立即执行
- 会执行所有微任务,直到微任务队列清空
- 微任务执行过程中产生的新微任务也会在当前循环中执行
- 优先级高于宏任务
五、经典面试题解析
5.1 基础题 - 执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出: 1 4 3 2
执行流程分析:
1. 执行同步代码: console.log('1') → 输出 1
2. 遇到 setTimeout,放入宏任务队列: [setTimeout]
3. 遇到 Promise.then,放入微任务队列: [then]
4. 执行同步代码: console.log('4') → 输出 4
5. 调用栈清空,执行微任务: console.log('3') → 输出 3
6. 微任务清空,执行下一个宏任务: console.log('2') → 输出 2
5.2 进阶题 - 嵌套异步
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise2');
setTimeout(() => {
console.log('timeout2');
}, 0);
});
console.log('end');
// 输出: start end promise2 timeout1 promise1 timeout2
执行流程分析:
第一轮 Event Loop:
1. 同步代码: start, end
2. 微任务队列: [promise2]
3. 宏任务队列: [timeout1]
执行微任务 promise2:
- 输出 promise2
- 添加宏任务 timeout2 到队列: [timeout1, timeout2]
第二轮 Event Loop:
执行宏任务 timeout1:
- 输出 timeout1
- 添加微任务 promise1 到队列: [promise1]
- 执行微任务 promise1: 输出 promise1
第三轮 Event Loop:
执行宏任务 timeout2:
- 输出 timeout2
5.3 高级题 - async/await
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 输出: script start, async1 start, async2, promise1, script end, async1 end, promise2, setTimeout
关键理解点:
// await async2() 等价于:
Promise.resolve(async2()).then(() => {
console.log('async1 end');
});
// 所以 'async1 end' 是微任务
5.4 陷阱题 - 微任务产生新微任务
Promise.resolve().then(() => {
console.log('promise1');
Promise.resolve().then(() => {
console.log('promise2');
});
}).then(() => {
console.log('promise3');
});
Promise.resolve().then(() => {
console.log('promise4');
});
// 输出: promise1 promise4 promise2 promise3
微任务队列变化:
初始微任务队列: [then1, then4]
执行 then1:
- 输出 promise1
- 添加 then2 到队列: [then4, then2]
- 返回新 Promise,添加 then3 到队列: [then4, then2, then3]
执行 then4:
- 输出 promise4
执行 then2:
- 输出 promise2
执行 then3:
- 输出 promise3
六、Node.js 中的 Event Loop
6.1 Node.js Event Loop 的六个阶段
┌───────────────────────────┐
┌─>│ timers │ 执行 setTimeout/setInterval 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 执行延迟到下一轮的 I/O 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ 检索新的 I/O 事件
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ 执行 setImmediate 回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ 关闭回调,如 socket.on('close')
└───────────────────────────┘
6.2 Node.js 微任务优先级
// Node.js 中微任务执行顺序
process.nextTick(() => {
console.log('nextTick');
});
Promise.resolve().then(() => {
console.log('Promise');
});
queueMicrotask(() => {
console.log('queueMicrotask');
});
// 输出: nextTick, Promise, queueMicrotask
// process.nextTick 优先级最高
6.3 setImmediate vs setTimeout
// 在 I/O 回调中,setImmediate 总是先于 setTimeout 执行
const fs = require('fs');
fs.readFile('./file.txt', () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
// 输出: setImmediate, setTimeout
七、实战应用场景
7.1 性能优化 - 大数据渲染
// 错误示范:一次性渲染10000条数据,页面卡顿
function renderBadly(data) {
data.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
document.body.appendChild(div);
});
}
// 优化方案:利用 Event Loop 分批渲染
function renderOptimized(data, batchSize = 100) {
let index = 0;
function renderBatch() {
const batch = data.slice(index, index + batchSize);
batch.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
document.body.appendChild(div);
});
index += batchSize;
if (index < data.length) {
// 使用 setTimeout 让出控制权,避免长时间占用主线程
setTimeout(renderBatch, 0);
}
}
renderBatch();
}
7.2 防抖与节流的微任务实现
// 使用微任务实现更精准的防抖
function debounceWithMicrotask(fn, delay) {
let timer = null;
let pending = false;
return function(...args) {
if (timer) clearTimeout(timer);
if (!pending) {
pending = true;
queueMicrotask(() => {
pending = false;
});
}
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用
const handleInput = debounceWithMicrotask((e) => {
console.log('搜索:', e.target.value);
}, 500);
7.3 Promise 串行执行
// 利用微任务特性实现 Promise 串行执行
async function serialExecute(tasks) {
const results = [];
for (const task of tasks) {
// await 确保每个任务按顺序执行
const result = await task();
results.push(result);
}
return results;
}
// 使用
const tasks = [
() => fetch('/api/1'),
() => fetch('/api/2'),
() => fetch('/api/3')
];
serialExecute(tasks).then(results => {
console.log('所有请求完成', results);
});
7.4 竞态条件处理
// 利用闭包和微任务解决搜索请求竞态问题
function createSearchHandler() {
let latestRequestId = 0;
return async function search(keyword) {
const requestId = ++latestRequestId;
try {
const result = await fetch(`/api/search?q=${keyword}`);
// 只处理最新的请求结果
if (requestId === latestRequestId) {
return result.json();
}
} catch (error) {
if (requestId === latestRequestId) {
throw error;
}
}
};
}
const search = createSearchHandler();
八、常见误区与陷阱
8.1 误区1: setTimeout(fn, 0) 会立即执行
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 输出: 1 3 2
// setTimeout 总是在下一轮 Event Loop 执行
8.2 误区2: Promise.then 是同步的
console.log('1');
new Promise((resolve) => {
console.log('2'); // Promise 构造函数是同步的
resolve();
}).then(() => {
console.log('3'); // then 是微任务
});
console.log('4');
// 输出: 1 2 4 3
8.3 误区3: async 函数会等待内部所有代码执行
async function test() {
console.log('1');
setTimeout(() => console.log('2'), 0);
await Promise.resolve();
console.log('3');
}
test();
console.log('4');
// 输出: 1 4 3 2
// await 后面的代码是微任务,会在同步代码之后、宏任务之前执行
8.4 陷阱: 死循环微任务
// 危险代码:会导致页面卡死
function infiniteMicrotasks() {
Promise.resolve().then(() => {
infiniteMicrotasks();
});
}
infiniteMicrotasks(); // 微任务队列永远无法清空,宏任务无法执行
九、面试高频考点总结
9.1 执行顺序判断技巧
- 第一步: 找出所有同步代码,按顺序执行
- 第二步: 标记所有微任务(Promise.then、async/await、queueMicrotask)
- 第三步: 标记所有宏任务(setTimeout、setInterval)
- 第四步: 按照”同步 → 微任务 → 宏任务 → 微任务…”的循环执行
9.2 必背知识点
- Event Loop 是协调 JavaScript 单线程执行异步任务的机制
- 每次 Event Loop 执行一个宏任务,但会清空所有微任务
- 微任务优先级高于宏任务
- Promise 构造函数是同步的,then/catch/finally 是微任务
- async/await 本质是 Promise 的语法糖,await 后面的代码是微任务
- Node.js 中 process.nextTick 优先级最高
9.3 实战输出题
// 综合练习题
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
});
setTimeout(() => {
console.log('7');
Promise.resolve().then(() => {
console.log('8');
});
}, 0);
Promise.resolve().then(() => {
console.log('9');
});
console.log('10');
// 输出: 1 4 10 5 9 2 3 7 8 6
十、工作中的最佳实践
10.1 合理使用异步
// ✅ 好的实践
async function fetchUserData(userId) {
try {
const user = await fetch(`/api/users/${userId}`);
const orders = await fetch(`/api/orders?userId=${userId}`);
return { user, orders };
} catch (error) {
console.error('获取数据失败', error);
}
}
// ❌ 不好的实践:过度使用 await 导致串行执行
async function fetchUserDataBad(userId) {
const user = await fetch(`/api/users/${userId}`);
const orders = await fetch(`/api/orders?userId=${userId}`); // 等待上一个完成
// 应该使用 Promise.all 并行执行
}
10.2 避免回调地狱
// ❌ 回调地狱
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
});
});
});
// ✅ 使用 async/await
async function fetchData() {
const a = await getData();
const b = await getMoreData(a);
const c = await getEvenMoreData(b);
console.log(c);
}
10.3 性能监控
// 监控 Event Loop 阻塞
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const delay = now - lastTime - 1000;
if (delay > 100) {
console.warn(`Event Loop 被阻塞了 ${delay}ms`);
}
lastTime = now;
}, 1000);
总结
Event Loop 是 JavaScript 异步编程的核心机制,理解它的运行原理对于写出高性能、无 bug 的代码至关重要。记住以下关键点:
- JavaScript 是单线程,通过 Event Loop 实现异步
- 微任务优先级高于宏任务,会在当前宏任务执行完后立即清空
- 每次 Event Loop 只执行一个宏任务,但会清空所有微任务
- Promise 构造函数是同步的,then/catch/finally 是微任务
- await 后面的代码是微任务,会在当前同步代码执行完后执行