DrillLab
第 56 / 105 道56 / 105 · #330

虚拟 DOM 和 diff 算法

Virtual DOM and diffing algorithm

先自己答,再往下看Answer it yourself first

一句话:虚拟 DOM 是用普通 JS 对象描述真实 DOM 的一棵轻量树。 状态变了先在内存里生成新树, 和旧树 diff,算出最小改动,再一次性打到真实 DOM 上

为什么快 —— 说准这两条:

  • 批量—— 十次 state 更新 合并成一次 DOM 操作, 避免十次重排(见 #288)。
  • 最小化—— 只改真正变了的属性和节点, 不重建整棵子树。

但要说出这层真相(加分点):虚拟 DOM 不一定比手写 DOM 快—— 精心手写的原生操作永远更快, 虚拟 DOM 还额外付出了「建树 + diff」的开销。它真正的价值是「在保持声明式写法的同时, 性能仍然够好」—— 是可维护性和性能的折中, 不是性能银弹。

diff 的三条启发式规则(把 O(n³) 降到 O(n) 的关键):

  1. 只比同层,不跨层移动节点。 跨层的话就是删了重建。
  2. 类型不同直接整棵重建——div 换成 span, 子树全部丢弃重做(state 也丢)。
  3. 同层列表用 key 认身份

key 为什么不能用 index—— 这是 React 面试最实用的一条: 在开头插入或删除一项时, 所有元素的 index 都变了, React 会认为「每一项的内容都变了」, 于是大量误更新; 更糟的是非受控输入框的内容会串到别的行, 因为 DOM 节点被复用了。Q1 那道真题里删除笔记的 bug 就是这个。

In one line: the virtual DOM is a lightweight tree of plain JS objects describing the real DOM. When state changes, React builds a new tree in memory, diffs it against the old one, works out the smallest set of changes, and applies them to the real DOM in one go.

Why it is fast — get these two right:

  • Batching — ten state updates collapse into one DOM write, so you avoid ten reflows (see #288).
  • Minimising — only the attributes and nodes that really changed get touched; whole subtrees are not rebuilt.

But say this part too — it is the bonus point: the virtual DOM is not necessarily faster than hand-written DOM code — carefully tuned native operations always win, and the virtual DOM pays extra for building a tree and diffing it. Its real value is that you keep the declarative style and performance is still good enough — it is a trade-off between maintainability and performance. It is not the right choice everywhere.

The three diff heuristics (what turns O(n³) into O(n)):

  1. Compare the same level only; nodes never move across levels. Crossing a level means delete and rebuild.
  2. A different type rebuilds the whole subtree — swap a div for a span and the subtree is thrown away and redone, state included.
  3. Lists on the same level use key for identity.

Why index must not be the key — the most useful thing you can say in a React interview: insert or delete at the front and every index shifts, so React believes the content of every row changed and does a pile of needless updates; worse, text typed into an uncontrolled input ends up on the wrong row, because the DOM node got reused. The delete-a-note bug in the real Q1 question is exactly this.

JSXkey 的选择Choosing the key示意Illustrative
1// 虚拟 DOM 就是普通对象
2{ type: "button", props: { className: "btn", children: "点我" } }
3
4// ✗ index 当 key:在开头插一项,所有 key 都变了
5{todos.map((t, i) => <Row key={i} todo={t} />)}
6
7// ✓ 稳定的业务 id
8{todos.map((t) => <Row key={t.id} todo={t} />)}
1// The virtual DOM is just a plain object
2{ type: "button", props: { className: "btn", children: "Click me" } }
3
4// ✗ index as key: insert one at the front and every key changes
5{todos.map((t, i) => <Row key={i} todo={t} />)}
6
7// ✓ a stable id from the data
8{todos.map((t) => <Row key={t.id} todo={t} />)}
只有「列表永不重排、不增删中间项」时 index 才安全。既然多数列表都会变,直接养成用 id 的习惯。An index is only safe when the list is never reordered and no item is inserted or removed in the middle. Most lists do change, so make using an id your default habit.