Vue Diff算法通过同层比对、双端预处理、key驱动复用和就地patch实现最小化DOM更新;Vue 3进一步引入静态标记与LIS算法优化性能。

局部刷新在高频动态场景下,核心矛盾不是“要不要更新”,而是“怎么让每次更新只动最少的DOM节点、最小的像素区域、最短的JS执行路径”。盲目套用 innerHTML 或反复 appendChild 会立刻卡死——这不是代码写得不够“现代”,是没把 DOM 变更压缩到数学意义上的最小集。
如何用 diff 算法识别真实 DOM 差异而非字符串差异
直接比对前后 HTML 字符串(如 oldHtml !== newHtml)几乎无意义:空格、换行、属性顺序、自动闭合标签都会触发误判;而 DOM 层面的变更可能只是某一行 textContent 改了,或某个 dataset.status 值翻转。
- 必须基于真实节点结构做轻量 diff:只比对关键字段——
tagName、textContent、dataset键值对、className、style.cssText(若涉及动态样式) - 跳过不影响渲染的属性:如
id(除非用于定位)、data-id(仅作标识)、tabindex(除非焦点逻辑依赖) - 对列表类结构(如
<ul>),先按data-key或key属性做稳定映射,再逐项比对;无 key 时退化为位置序号比对,但要加警告日志 - 避免递归全树遍历:对子节点数 > 50 的容器,只 diff 首尾 5 个 + 所有带
data-dirty的节点,其余标记为“未验证”并延后处理
为什么 documentFragment 不是万能解,而只是最小变更的前置条件
document.createDocumentFragment() 解决的是“插入开销”,不是“变更判定”。它无法帮你判断该删哪几个节点、该复用哪几个、该改哪些属性——这些必须在 fragment 构建前就决策好。
- fragment 必须按最小变更结果来组装:比如原列表有 100 项,新数据只改第 3 行和删第 45 行,那就只创建 2 个新
li+ 调用 1 次removeChild,而不是重建全部 100 个 - 不要在 fragment 里 clone 整个旧节点再改内容——cloneNode(true) 会复制所有事件监听器和内部状态,极易泄漏;应只复用节点引用,仅更新必要属性
- fragment 插入后立即清空,不可缓存复用;但可缓存“变更指令数组”,例如
[{op: 'update', node: li3, text: 'new text'}, {op: 'remove', node: li45}],下次 diff 直接复用指令逻辑
DOM 复用池与局部刷新的协同边界在哪
复用池(domPoolFactory)只管“节点对象生命周期”,不管“节点内容是否该更新”。两者必须严格分工,否则会出现内容残留或结构错乱。
立即学习“前端免费学习笔记(深入)”;
- 池中节点取出后,第一件事是调用
node.textContent = ''和node.innerHTML = ''——但仅此而已;不重置dataset或className,因为这些可能是本次刷新需要保留或设置的 - 回收节点前,必须手动解绑所有事件:
node.removeEventListener('click', handler);不能依赖innerHTML = ''自动清理,IE11 和部分 Safari 版本不会释放闭包引用 - 池只适用于固定结构模板(如表格行、日志条目);对于内容差异大、结构不稳定的区域(如富文本编辑区),强制复用反而增加 diff 复杂度,应直接销毁重建
高频场景下最容易被忽略的“隐式变更”陷阱
很多掉帧问题不来自你写的更新逻辑,而来自浏览器在你不知情时触发的隐式布局计算或样式重算。
- 读取
offsetHeight、getBoundingClientRect()、computedStyle后立刻写 DOM → 强制同步 reflow,打断批量更新节奏 - 在
requestAnimationFrame回调里多次修改同一元素的style.left和style.top→ 每次都可能触发 layout,应合并为style.transform = 'translate(10px, 20px)' - 使用
innerHTML替换含<input>的片段时,未保留value和checked状态 → 浏览器重置控件,用户输入丢失,被迫加额外状态同步逻辑 - 服务端返回的 HTML 片段里含内联
<script>标签 → 浏览器执行脚本时阻塞解析,且无法控制执行时机;应禁用服务端 script 输出,改用事件委托或 data 属性驱动行为
最小 DOM 变更不是靠工具链堆出来的,是靠每一层都守住自己的责任边界:diff 层只输出指令,复用池只管理对象,fragment 只承载结果,而渲染层只执行一次插入。任何一层越界承担别层职责,算法就会从“最小”滑向“不可控”。



















