直接操作 DOM 的代价:每次修改都可能引发回流重绘,且代码里「如何改」散落一地。虚拟 DOM(一层 JS 对象描述)把「改 DOM」拆成两步:① 生成新虚拟树与旧树做 diff 得到差异补丁(patch);② 一次性把补丁应用到真实 DOM。中间多一层抽象,换来「声明式数据驱动」的编程模型与跨平台能力(React Native、SSR)。
Diff 的三大假设让算法从 O(n³) 降到 O(n):① 同层比较(不跨层级移动);② 类型不同直接替换整个子树(标签变了重建);③ 列表靠 key 标识复用(key 是身份标识不是索引)。新节点树上每个节点位置稳定,用 key 判断「是复用还是新建删除」——这是没有 key 或用 index 做 key 重渲染出错的根本原因。
// 虚拟 DOM 长什么样 const vnode = { tag: 'ul', props: { class: 'list' }, children: [ { tag: 'li', props: { key: 'a' }, children: ['第 1 项'] }, { tag: 'li', props: { key: 'b' }, children: ['第 2 项'] }, ], }; // diff 的核心判定 function diff(oldVn, newVn) { if (oldVn.tag !== newVn.tag) { return { type: 'replace', vnode: newVn }; // 标签不同 → 整树替换 } if (oldVn === newVn) return { type: 'skip' }; // 同引用 → 跳过 // ... props diff、children diff(列表用 key 复用) } // 列表 key 的价值(为什么 index 做 key 不行): // [A, B, C] → [C, A, B](C 插队到前面) // 有 key:移动 C 的真实节点(复用,顺序按新数组调整)✓ // 无 key/index:按位置逐个 diff → A 被改成 C、B 改成 A…原地更新 // 后果:输入框等带状态的子组件状态错乱 + 全部节点重绘