Appearance
1. 背景回顾:为什么会“漏刷”
本节目标:理解“1. 背景回顾:为什么会“漏刷””的核心思路,并能把它用于实际开发或面试表达。
说明:原 OneNote 中有图片引用,但图片未包含在导出目录中;本页保留了可用的文字与代码内容。
flushAll在一次requestAnimationFrame(rAF)回调内遍历priceQueue,对 多个 symbol 连续赋值同一个响应式字段newQuotePayload: this.newQuotePayload = { symbol, quote: last };
Vue 为了性能会把同一宏任务(macrotask)里对同一响应式字段的多次写入合并,只触发最后一次 watch;
于是同帧内除了最后一个 symbol 之外,其余的报价都被“覆盖”了,界面得不到更新。
- 解决思路
- 本帧内先把每个 symbol 的最新报价收集到
batch。 - 再把 每条报价各自推入微任务队列(microtask queue)。
- 这样就把一次宏任务拆成了 N 次相邻微任务。
- Vue 的 watch 会在每个微任务结尾执行,因此 每个 symbol 都能独立触发一次 watch。
对应改动(节选) // 收集 batch.push({ symbol, quote: last }); // 派发 batch.forEach(({ symbol, quote }) => { const dispatch = () => { this.newQuotePayload = { symbol, quote } as any; }; if (typeof queueMicrotask === 'function') { queueMicrotask(dispatch); // 推荐 } else { Promise.resolve().then(dispatch); // 回退 } }); 3. 事件循环(Event Loop)全景
浏览器的执行模型可以抽象成事件循环 → 渲染循环的交替:
- 取出一个宏任务(macrotask)执行。
- Microtask Check-point:不断取出并执行所有微任务,直到清空。
- 期间如果又加入了新的微任务(例如 then 里再 then),也会立刻继续执行。
- 执行 render 阶段:
- 浏览器更新样式计算(Layout)、绘制(Paint)、合成(Composite)。
- 此阶段会调用 rAF 回调,为下一帧 JavaScript 预留入口。
返回步骤 1,进入下一轮循环。
queueMicrotask — 微任务的“官方”入口
| 特性 | 说明 |
| 规范 | WHATWG HTML Standard,现代浏览器均支持(Chrome 71+,Edge 79+,Firefox 69+,Safari 13+)。 |
| 作用 | 将回调排进 当前宏任务结束前 必执行的微任务队列。 |
| 与 Promise.then 区别 | <br>• API 直接、语义更清晰;<br>• 不涉及生成冗余的 Promise 对象;<br>• 部分环境(老 Node)里不依赖原生 Promise 也可用。 |
| 与 setTimeout 区别 | setTimeout 属于 宏任务,至少延后到下一轮事件循环;queueMicrotask 属于 微任务,本轮必执行且早于渲染。 |
用法 queueMicrotask(() => { // 在当前宏任务结束、渲染之前执行 }); 浏览器不支持时,可退化成 Promise.resolve().then(...),效果等同。
- requestAnimationFrame 与渲染时序 ┌ 上一帧渲染完毕 │ ├─(1) 调用 rAF 回调 ← 这里我们执行 flushAll() │ ├─(2) 执行 microtask queue ← 我们新建的 queueMicrotask 在此阶段运行 │ └─(3) 浏览器进行下一次渲染
因此我们的修改保证:
- 同一帧:所有 symbol 的报价都能通过各自的微任务触发 Vue watcher。
- 渲染前:UI 已经完成数据 diff,DOM patch,达到一致性。
- 下一帧:浏览器把补丁一次性绘制,避免多次布局/重绘。
6. Vue3 Reactivity + Watch 的调度细节
- Vue 自己也会把响应式副作用包装进微任务,等本轮 flush 一起触发。
- 如果你在同一个微任务里多次写入同一 ref/prop,Vue 会合并,只触发一次 watcher。
- 将写入拆到不同的微任务——即我们现在的做法——则每次都会触发一次独立的副作用更新。
7. 方案优缺点
| 方案 | 优点 | 缺点 |
| 微任务拆分(现方案) | • 改动极小;<br>• watch 代码几乎不用动;<br>• 不改变数据结构 | • 对极大量 symbol 每帧都拆分微任务时,可能带来微任务风暴(一般行情推送频率较低,影响可忽略)。 |
| 把 newQuotePayload 改成 Map/Array | • 一帧一次 watch;更易做批处理 | • 需要改动所有 watch 端(OpenOrder、PendingOrder、行情表格等);<br>• Watch 逻辑要做遍历与过滤,代码量更大。 |
| rAF 多帧轮询 | • 实现简单 | • 若 symbol 很多,会拖长整体刷新延迟;体验不佳。 |
