页面的loading没有立即生效的引申了解vue的更新机制和js事件循环(2)

143 阅读3分钟

JavaScript 的事件循环(Event Loop)是其实现异步编程的核心机制,它决定了代码的执行顺序,特别是在处理异步任务(如 setTimeoutPromiseI/O 操作等)时。理解事件循环对优化性能、避免 UI 阻塞以及解决类似你的 loading 状态问题非常重要。


1. 事件循环的基本概念

JavaScript 是单线程的,意味着它一次只能执行一个任务。事件循环负责管理任务队列,确保异步任务在适当的时候执行。它主要由以下几个部分组成:

(1)调用栈(Call Stack)

  • 用于跟踪当前正在执行的函数(同步代码)。
  • 当函数被调用时,它会被推入调用栈;执行完毕后,会被弹出。

(2)任务队列(Task Queue / Callback Queue)

  • 存放异步任务的回调函数(如 setTimeoutDOM 事件I/O 操作)。
  • 当调用栈为空时,事件循环会从任务队列中取出任务执行。

(3)微任务队列(Microtask Queue)

  • 存放 Promise.thenMutationObserverqueueMicrotask 等微任务。
  • 微任务优先级高于宏任务,会在当前宏任务执行完后立即执行所有微任务。

2. 事件循环的执行流程

  1. 执行同步代码(调用栈中的任务)。
  2. 调用栈为空时,检查微任务队列:
    • 执行所有微任务(如 Promise.then)。
  3. 微任务执行完后,取出一个宏任务(如 setTimeout)执行。
  4. 重复上述过程(循环)。

🌰 示例

console.log("1"); // 同步

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => console.log("3")); // 微任务

console.log("4"); // 同步

输出顺序

1 → 4 → 3 → 2

解释

  1. 同步代码 14 先执行。
  2. 微任务 3 优先于宏任务 2 执行。

3. 为什么你的 loading 状态没有立即显示?

第一种写法的问题

loading.value = true; // (A) 触发 Vue 更新(微任务)
tableData.value.push(...data); // (B) 同步耗时操作
setTimeout(() => { // (C) 宏任务
  loading.value = false;
}, 2000);

执行顺序

  1. (A) 修改 loading,Vue 的响应式更新是微任务(类似 Promise.then),不会立即渲染。
  2. (B) 执行大数据操作(同步),阻塞主线程,浏览器无法渲染 UI。
  3. (C) 2 秒后关闭 loading,但用户根本没看到它出现。

第二种写法为什么有效?

loading.value = true; // (A) 触发 Vue 更新(微任务)
setTimeout(() => { // (B) 宏任务
  tableData.value.push(...data); // (C) 同步操作
  setTimeout(() => { // (D) 宏任务
    loading.value = false;
  }, 0);
}, 0);

执行顺序

  1. (A) 修改 loading,Vue 的更新进入微任务队列。
  2. (B) 把大数据操作放入宏任务队列(setTimeout),先让 UI 更新
  3. 浏览器渲染 loading(因为同步代码执行完,微任务执行 Vue 更新)。
  4. 执行 (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 渲染优化! 🚀