JavaScript 的事件循环(Event Loop)是其实现异步编程的核心机制,它决定了代码的执行顺序,特别是在处理异步任务(如 setTimeout、Promise、I/O 操作等)时。理解事件循环对优化性能、避免 UI 阻塞以及解决类似你的 loading 状态问题非常重要。
1. 事件循环的基本概念
JavaScript 是单线程的,意味着它一次只能执行一个任务。事件循环负责管理任务队列,确保异步任务在适当的时候执行。它主要由以下几个部分组成:
(1)调用栈(Call Stack)
- 用于跟踪当前正在执行的函数(同步代码)。
- 当函数被调用时,它会被推入调用栈;执行完毕后,会被弹出。
(2)任务队列(Task Queue / Callback Queue)
- 存放异步任务的回调函数(如
setTimeout、DOM 事件、I/O操作)。 - 当调用栈为空时,事件循环会从任务队列中取出任务执行。
(3)微任务队列(Microtask Queue)
- 存放
Promise.then、MutationObserver、queueMicrotask等微任务。 - 微任务优先级高于宏任务,会在当前宏任务执行完后立即执行所有微任务。
2. 事件循环的执行流程
- 执行同步代码(调用栈中的任务)。
- 调用栈为空时,检查微任务队列:
- 执行所有微任务(如
Promise.then)。
- 执行所有微任务(如
- 微任务执行完后,取出一个宏任务(如
setTimeout)执行。 - 重复上述过程(循环)。
🌰 示例
console.log("1"); // 同步
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
console.log("4"); // 同步
输出顺序:
1 → 4 → 3 → 2
解释:
- 同步代码
1和4先执行。 - 微任务
3优先于宏任务2执行。
3. 为什么你的 loading 状态没有立即显示?
第一种写法的问题
loading.value = true; // (A) 触发 Vue 更新(微任务)
tableData.value.push(...data); // (B) 同步耗时操作
setTimeout(() => { // (C) 宏任务
loading.value = false;
}, 2000);
执行顺序:
(A)修改loading,Vue 的响应式更新是微任务(类似Promise.then),不会立即渲染。(B)执行大数据操作(同步),阻塞主线程,浏览器无法渲染 UI。(C)2 秒后关闭loading,但用户根本没看到它出现。
第二种写法为什么有效?
loading.value = true; // (A) 触发 Vue 更新(微任务)
setTimeout(() => { // (B) 宏任务
tableData.value.push(...data); // (C) 同步操作
setTimeout(() => { // (D) 宏任务
loading.value = false;
}, 0);
}, 0);
执行顺序:
(A)修改loading,Vue 的更新进入微任务队列。(B)把大数据操作放入宏任务队列(setTimeout),先让 UI 更新。- 浏览器渲染
loading(因为同步代码执行完,微任务执行 Vue 更新)。 - 执行
(C)大数据操作,再在(D)关闭loading。
4. 如何优化大数据渲染?
(1)使用 requestAnimationFrame
loading.value = true;
requestAnimationFrame(() => {
tableData.value.push(...data);
loading.value = false;
});
requestAnimationFrame会在浏览器渲染前执行,比setTimeout更适合 UI 更新。
(2)分批加载(Chunking)
async function loadData(data) {
loading.value = true;
for (let i = 0; i < data.length; i += 100) {
const chunk = data.slice(i, i + 100);
tableData.value.push(...chunk);
await new Promise(resolve => setTimeout(resolve, 0)); // 让 UI 有机会更新
}
loading.value = false;
}
- 避免一次性渲染大量数据,分批加载可保持 UI 响应。
(3)使用 Web Workers
const worker = new Worker("data-worker.js");
worker.postMessage(data);
worker.onmessage = (e) => {
tableData.value = e.data;
loading.value = false;
};
- 把大数据处理放到后台线程,避免阻塞主线程。
5. 总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
loading 不显示 | 大数据同步操作阻塞 UI 渲染 | setTimeout/requestAnimationFrame 让 UI 先更新 |
| 界面卡顿 | 大数据一次性渲染 | 分批加载(Chunking) |
| 主线程阻塞 | 复杂计算占用 JS 线程 | Web Workers 后台计算 |
关键点:
- 同步代码会阻塞 UI 渲染,大数据操作要异步化。
- 微任务(
Promise)比宏任务(setTimeout)优先级高。 requestAnimationFrame更适合 UI 更新,比setTimeout更高效。
希望这个解释能帮你理解事件循环和 UI 渲染优化! 🚀