跳到主要内容

第 18 章:属性与 DOM 更新——style、事件、ref

第 17 章说 mutation 阶段改 DOM。本章看它具体怎么改一个元素:新旧 props 怎么算出差集、style 怎么处理、事件和 ref 为什么不在这一步直接绑。

先记住这一句:DOM 属性更新就一条原则:只改变化的,不碰没变的。style 拆到 CSS 属性级去比;事件根本不在这里绑(走第 19 章委托);ref 不在这里赋值(在 layout 阶段)。

一图流:一次属性更新的分派

updateProperties:只算差异

mutation 阶段对 HostComponent 调用 commitUpdate,最后落到 updatePropertiesReactDOMComponent.js:1433)。它拿 lastProps(上次的)和 nextProps(这次的),逐个属性算出要增删改什么,再对 DOM 调用对应的 set/remove 操作。

源码里对标签做了快路径(1443):divspansvgpathagpli 这些最常见标签直接走通用逻辑,省掉一次分支判断。而 input 这种有状态标签单独处理:valuechecked 这些受控属性不走通用 setAttribute,而是通过 updateWrapper 统一写(保证输入框的行为符合 React 的受控语义)。

style:diff 到 CSS 属性级

style 是特殊 props,React 不会整个 style.cssText 覆盖,而是逐条 CSS 属性对比:lastProps.stylenextProps.style 里每个键,只对变化的调用 style.setProperty,对消失的调用 style.removeProperty。这样你在渲染里写 style={{marginLeft: 10}},React 只动 margin-left 一条,其他样式原封不动。这也是 React 的 style 性能优于手动 style.cssText 覆盖的原因之一。

事件:根本不在这里绑

onClick 这类 props 在 updateProperties不会 addEventListener。React 19 的事件处理是全局委托:所有支持的事件在 createRoot 时就统一绑定到了根容器(第 19 章的主角),元素上只有"这个 fiber 想监听什么"的记录。所以你给元素换 onClick 时,不需要解绑再绑,只是改一下内部记录。这就是第 19 章要展开的机制。

ref:在 layout 阶段赋值

ref 的赋值也不在 mutation。commitLayoutEffectOnFiberReactFiberCommitWork.js:592)在 layout 阶段做:DOM 改完了、实例就位了,才把 DOM 节点挂进 ref.current(或调用 ref 回调)。所以 ref 回调里能安全读 DOM,是因为它保证跑在 DOM 变更之后、paint 之前。

动手实验

  1. 看差异计算:在 updateProperties(ReactDOMComponent.js:1433)打断点,更新一个元素的 classNamestyle.margin,看它是只对变化的两处调用写操作。
  2. 验证 style 粒度:第一次渲染 style={{width: 10}},第二次 style={{width: 10, height: 5}},断点观察 height 被 set,width 没被碰。
  3. 验证 ref 时机:ref 回调里 console.log('ref', el.offsetWidth),对比 useEffect 里的 offsetWidth,确认 ref 在 layout 阶段(paint 前)拿到 DOM。

小结

  • 属性更新原则:只改变化的updateProperties(1433)逐属性算差集。
  • 常用标签走快路径,inputvalue/checked 特殊处理。
  • style 拆到 CSS 属性级 diff,只动变化的。
  • 事件不在这里绑,走全局委托(第 19 章)。
  • ref 在 layout 阶段赋值(commitLayoutEffectOnFiber:592),保证能安全读 DOM。
  • 下一步:事件委托到底怎么工作,第 19 章。