JavaScript 在一个执行线程上运行用户代码,但浏览器还提供网络、定时器、渲染等能力。异步任务完成后会进入队列,由事件循环在合适的时机交给 JavaScript 执行。

一次循环发生什么

1
2
3
4
5
执行当前同步代码
-> 清空 Microtask 队列
-> 浏览器可能进行样式、布局和绘制
-> 取下一个 Task 执行
-> 再次清空 Microtask

常见 Task 包括点击事件、定时器和网络事件;Promise 回调和 queueMicrotask 属于 Microtask。Microtask 持续递归加入自己,也可能让页面长时间无法渲染。

1
2
3
4
5
6
console.log('A')

setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))

console.log('D')

输出通常是 A D C B:同步代码先执行,Promise Microtask 先于下一个定时器 Task。

async/await 没有阻塞整个浏览器

1
2
3
4
async function loadUser() {
const response = await fetch('/api/user')
return response.json()
}

await 会暂停当前异步函数,控制权返回事件循环;网络由浏览器处理,结果就绪后再安排后续代码。它看起来像同步流程,但错误、取消和并发仍要显式处理。

一次 Vue 交互

1
2
3
4
5
6
用户点击
-> 事件处理函数修改响应式状态
-> Vue 调度更新任务
-> 同一轮多次修改被批处理
-> 组件重新计算并更新 DOM
-> 浏览器完成布局和绘制

DOM 更新通常不是在赋值语句后立即完成;确实需要等待更新结果时使用 nextTick,不要到处用定时器猜测渲染时机。

状态应放在哪里

状态 合适位置
输入框、弹窗、局部选中 组件本地
页面筛选和分页 页面或 URL
用户身份、主题等跨页面状态 全局 Store
接口数据、缓存和重新验证 请求缓存层
必须跨刷新保留的数据 服务端或受控本地存储

不要把所有接口结果复制进全局 Store,否则容易出现服务端数据与前端副本不同步。

常见性能问题

  • 长任务阻塞事件循环,导致点击无响应。
  • 大列表一次渲染,造成布局和绘制开销。
  • Watcher 或 Effect 形成循环更新。
  • 高频输入、滚动事件触发过多计算和请求。
  • 在主线程进行大图处理或复杂序列化。

优先通过 Performance 工具找到长任务,再考虑拆分任务、虚拟列表、Web Worker、节流或减少响应式范围。

延伸阅读

站内搜索

没有找到内容!