深入理解 JavaScript 事件循环(Event Loop)与异步编程机制
一、事件循环的本质是什么?
JavaScript 是单线程语言,主线程负责执行所有的任务,但它并不会阻塞异步操作。这一切都依赖于“事件循环”机制。
事件循环是浏览器和 Node.js 的核心调度器,负责将任务从任务队列(Task Queue)中调度到主线程执行。
二、宏任务 vs 微任务
JavaScript 中的任务队列分为两种:
类型 | 示例 |
---|---|
宏任务(Macro Task) | setTimeout 、setInterval 、setImmediate 、I/O、UI渲染 |
微任务(Micro Task) | Promise.then 、MutationObserver 、queueMicrotask |
执行顺序:
每次事件循环:
- 执行主线程中的同步任务。
- 清空微任务队列。
- 执行一个宏任务。
- 循环重复。
三、代码深入分析
来看一个常见例子:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出结果:
1
4
3
2
分析步骤:
- 同步执行
console.log('1')
- 注册一个
setTimeout
宏任务 - 注册一个
Promise.then
微任务 - 执行
console.log('4')
- 当前调用栈空,立即执行微任务
console.log('3')
- 然后进入下一轮事件循环,执行宏任务
console.log('2')
四、真实案例分析:异步加载优化
示例:异步加载多个模块但按顺序执行
async function loadModulesInOrder() {
await importModule('module1.js');
await importModule('module2.js');
await importModule('module3.js');
}
function importModule(url) {
return new Promise((resolve) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
document.body.appendChild(script);
});
}
- 利用了
Promise
和await
的微任务机制 - 每次
await
后会将剩余代码推入微任务队列,确保模块顺序执行
五、Node.js 中的事件循环
Node 的事件循环与浏览器略有不同,它的阶段包括:
- timers(执行
setTimeout/setInterval
回调) - pending callbacks
- idle, prepare
- poll(I/O 阶段)
- check(执行
setImmediate
) - close callbacks
- 微任务(每个阶段之后处理)
示例对比:
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
在 Node.js 中,这两者谁先执行并不确定,依赖于当前 poll 阶段是否为空。
六、异步控制最佳实践
1. 并发控制
async function limitConcurrency(tasks, limit = 5) {
const executing = new Set();
const run = async (task) => {
const p = task();
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) {
await Promise.race(executing);
}
return p;
};
for (const task of tasks) {
await run(task);
}
}
2. 防抖和节流(高频事件优化)
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
fn.apply(this, args);
}
};
}
七、面试高频题实战模拟
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('end');
输出:
start
end
promise1
promise2
setTimeout
考点:
- 同步任务优先
- 多级
then
都是微任务 - 宏任务最后执行
八、总结
关键点 | 描述 |
---|---|
事件循环 | 控制任务调度,单线程并发关键 |
微任务优先 | 微任务执行在每轮宏任务之后、下一轮宏任务之前 |
setTimeout ≠ 立即执行 | 即便 delay 为 0 也会延迟一轮事件循环 |
实践应用 | 并发控制、模块加载、性能优化 |
延伸阅读建议
- MDN: Event Loop
- Node.js 官方文档中的 Event Loop 阶段
- Chrome DevTools 的 Tasks、Microtasks 调试面板
- Jake Archibald 的经典事件循环图解