参与虚拟DOM diff的全局属性是class、style、id、tabindex、draggable、contenteditable;其中class和style被深度定制化处理,id仅用于节点复用,不触发DOM更新。

现代前端框架(Vue、React)对 HTML 全局属性的处理不是“统一合并”,而是按语义和底层 DOM 行为分层决策:有些属性走虚拟 DOM diff 流程,有些绕过 diff 直接写 property,还有些被完全忽略或仅作初始化用。
哪些全局属性参与 vdom diff?
只有具备“可变性 + 可比对性 + 框架可控性”的属性才进入 diff 算法。典型的是 class、style、id、tabindex、draggable、contenteditable。
class 和 style 是唯二被深度定制化处理的:
-
class:Vue/React 都会把字符串转为 Set 或 Map,逐项比对增删,避免全量重写className -
style:不会用setAttribute('style', ...),而是遍历对象键值,对旧值不存在的新 key 赋值,对新值不存在的旧 key 设为空字符串(el.style.color = '')
而 id 虽参与 diff,但只用于节点复用(key 逻辑),不触发任何 DOM 更新;tabindex 变更会同步到 focus tree,但不强制 layout。
立即学习“前端免费学习笔记(深入)”;
哪些属性绕过 diff 直接操作 DOM property?
hidden、disabled、checked、selected 这类布尔属性,框架一律跳过 diff,直接赋值给元素的 JS property:
-
el.hidden = true(不是setAttribute('hidden', '')) - 这导致它们无法被 patch 合并优化——哪怕前后 vnode 的
hidden值相同,只要 diff 后进入 commit 阶段,就会执行一次 property 写入 - 副作用明显:触发 layout(reflow),且 SSR hydration 时若服务端有
hidden属性而客户端初始状态为false,会多一次 layout
这也是为什么用 class="is-hidden" 切换显隐比 :hidden="flag" 更轻量——CSS 层更新不强制 layout。
哪些属性只读取、不响应、不 diff?
data-* 是最典型的“单向只读初始化通道”:
-
dataset只映射 HTML 初始值,后续 JS 修改el.setAttribute('data-id', '500')不会触发组件更新 - 所有值都是字符串:
data-count="42"→el.dataset.count === "42",不是数字 - 框架(如 Vue 的
v-bind)若绑定:data-id="value",实际是 setAttribute,但组件内部不会监听该变化
真正想让 data-* 参与响应式,必须走自定义元素路线:static get observedAttributes() 显式声明,并在 attributeChangedCallback 中手动同步状态。
容易被忽略的关键点
框架对属性的处理逻辑,本质上是由浏览器原生行为反推出来的——不是设计出来的规则,而是妥协出来的适配。
hidden 绕过 diff,是因为浏览器一设 el.hidden = true 就立刻从渲染树剔除节点;style 被精细 diff,是因为全量覆盖会丢 margin;data-* 不响应,是因为 DOM 规范没定义它的变更事件。
所以别指望“统一规则”,每个属性都要单独查它在真实 DOM 中怎么工作,再看框架怎么桥接。否则 hydration 错位、layout 暴增、状态不同步,全是必然结果。



















