JS 事件循环详解
文章目录
- JS 事件循环详解
- 一、JS 的单线程模型与异步机制
- 二、事件循环的核心组件
- 1. 执行栈(Call Stack)
- 2. 任务队列(Task Queue)
- 3. Web APIs
- 三、事件循环的执行流程
- 四、任务类型详解
- 1. 宏任务(Macrotask)
- 2. 微任务(Microtask)
- 五、经典执行顺序示例
- 六、实际应用场景
- 1. 定时任务控制
- 2. Promise 异步流程
- 3. DOM 事件优化
- 七、常见误区与最佳实践
- 1. 不要阻塞事件循环
- 2. 微任务嵌套陷阱
- 3. 合理使用任务优先级
一、JS 的单线程模型与异步机制
JS 是一种单线程语言,这意味着它只有一个主线程(执行栈)来处理所有任务。这种设计避免了多线程环境中的复杂同步问题,但也带来了一个挑战:如何防止长时间运行的代码阻塞整个程序?
解决方案是将代码分为:
- 同步代码:由 JS 引擎直接执行
- 异步代码:交给宿主环境(浏览器/Node.js)处理
二、事件循环的核心组件
1. 执行栈(Call Stack)
- 用于存储同步任务的执行上下文
- 遵循后进先出(LIFO)原则
- 当函数执行时会被推入栈顶,执行完毕后弹出
2. 任务队列(Task Queue)
- 宏任务队列(Macrotask Queue)
- 微任务队列(Microtask Queue)
3. Web APIs
- 浏览器提供的异步API(setTimeout、DOM事件等)
- Node.js 中的 I/O 操作等
三、事件循环的执行流程
-
执行同步代码:执行栈中的任务依次执行
-
处理微任务:
- 执行栈清空后,立即执行所有微任务
- 微任务执行期间产生的新微任务会继续执行
-
渲染更新(浏览器环境)
-
取一个宏任务执行
-
重复循环
四、任务类型详解
1. 宏任务(Macrotask)
来源 | 示例 |
---|---|
setTimeout/setInterval | setTimeout(fn, 0) |
I/O 操作 | 文件读写、网络请求 |
UI 渲染 | (浏览器) |
事件回调 | click , scroll 等 |
setImmediate | (Node.js 特有) |
特点:
- 每次事件循环只执行一个宏任务
- 优先级低于微任务
2. 微任务(Microtask)
来源 | 示例 |
---|---|
Promise | .then() /.catch() |
MutationObserver | DOM 变更观察 |
process.nextTick | (Node.js 特有,优先级最高) |
特点:
- 在当前宏任务结束后立即执行
- 会清空整个微任务队列
- 优先级高于宏任务
五、经典执行顺序示例
console.log('1. 同步代码开始');setTimeout(() => {console.log('6. 宏任务1 - setTimeout');Promise.resolve().then(() => {console.log('7. 微任务3 - Promise');});
}, 0);Promise.resolve().then(() => {console.log('3. 微任务1 - Promise');return Promise.resolve();
}).then(() => {console.log('4. 微任务2 - Promise');
});console.log('2. 同步代码结束');// 输出顺序:
// 1. 同步代码开始
// 2. 同步代码结束
// 3. 微任务1 - Promise
// 4. 微任务2 - Promise
// 6. 宏任务1 - setTimeout
// 7. 微任务3 - Promise
六、实际应用场景
1. 定时任务控制
// 动画帧控制
function animate() {// 动画逻辑requestAnimationFrame(animate); // 比setTimeout更适合动画
}
animate();// 轮询检查
function poll() {fetch('/api/status').then(checkStatus).then(() => setTimeout(poll, 5000));
}
2. Promise 异步流程
function loadData() {return fetch('/api/data').then(response => response.json()).then(data => {// 处理数据return processData(data);}).catch(error => {// 错误处理console.error(error);});
}
3. DOM 事件优化
// 防抖处理高频事件
function debounce(fn, delay) {let timer;return function() {clearTimeout(timer);timer = setTimeout(() => fn.apply(this, arguments), delay);};
}window.addEventListener('scroll', debounce(() => {// 处理滚动逻辑
}, 100));
七、常见误区与最佳实践
1. 不要阻塞事件循环
// 错误示范:同步计算阻塞UI
function heavyCalc() {let result = 0;for (let i = 0; i < 1000000000; i++) {result += Math.sqrt(i);}return result;
}// 正确做法:分片处理
async function chunkedHeavyCalc() {let result = 0;for (let i = 0; i < 100000000; i += 100000) {result += await chunkCalc(i, Math.min(i + 100000, 100000000));// 允许浏览器渲染await new Promise(resolve => requestAnimationFrame(resolve));}return result;
}
2. 微任务嵌套陷阱
// 可能导致无限循环
function microtaskLoop() {Promise.resolve().then(microtaskLoop);
}
// microtaskLoop(); // 不要这样做!
3. 合理使用任务优先级
// 需要立即执行的任务使用微任务
function urgentTask(callback) {Promise.resolve().then(callback);
}// 不紧急的任务使用宏任务
function backgroundTask(callback) {setTimeout(callback, 0);
}