React Ref绑定DOM的底层逻辑及异常场景解析
我想理解React ref操作DOM时的工作机制,下面基于React官网(v18.3.1)教程的修改示例展开问题:
两段代码的差异与报错问题
以下两段代码中,第一段抛出运行时错误,第二段可正常运行,底层发生了什么?
代码1(报错)
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); inputRef.current = 2; // 唯一差异 function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
报错信息:App.js: inputRef.current.focus is not a function (8:21)
注:该测试在React官网教程的交互代码区域进行,但此错误并非在所有版本中均可复现,推测是React非预期用法导致底层机制存在版本差异,合理预期应为不报错。
代码2(正常运行)
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); inputRef.current = null; // 唯一差异 function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
根据React官方文档:
React sets
ref.currentduring the commit. Before updating the DOM, React sets the affectedref.currentvalues tonull. After updating the DOM, React immediately sets them to the corresponding DOM nodes.
我理解为每次React更新时inputRef.current都会被重新赋值,但有两个疑问:
- 手动修改
inputRef.current的操作为何未被<input ref={inputRef}/>覆盖? - 为何赋值为2会报错?
第三个示例的疑问
另有一个重新赋值ref的示例:
import { useRef, useState } from 'react'; export default function Form() { const inputRef = useRef(null); const inputRef2 = useRef(null); const [state, setState] = useState(1); function handleClick() { inputRef.current.focus(); inputRef.current = inputRef2.current; setState(state+1); // 仅用于触发重新渲染 } return ( <> {state} <input ref={inputRef} /> // 每次重新渲染时会重置inputRef吗? <input ref={inputRef2} /> <button onClick={handleClick}> Focus the input </button> </> ); }
此示例中第一次点击聚焦第一个输入框,第二次点击聚焦第二个,<input ref={inputRef}/>在每次重新渲染时具体执行了什么操作?
解答
一、前两段代码的差异原因
首先要明确:React在组件首次渲染和更新的commit阶段才会处理ref的赋值,而组件函数体中的代码是在渲染阶段执行的。
代码1报错的逻辑
- 组件首次渲染时,函数体先执行:
inputRef.current = 2,把ref的current设为数字2。 - 进入commit阶段,React处理
<input ref={inputRef}>:- 首次渲染属于创建DOM而非更新DOM,所以不会触发官方文档中"更新DOM前设为null"的逻辑。
- 部分React版本存在边界处理:如果ref.current已被手动设置为非null、非DOM节点的值,React会判定为开发者主动操作,跳过将DOM节点赋值给它的步骤。
- 最终
inputRef.current保留了手动设置的2,点击按钮调用focus()自然报错,因为数字没有这个方法。
- 组件首次渲染时,函数体先执行:
代码2正常运行的逻辑
- 组件首次渲染时,函数体执行
inputRef.current = null,和useRef的初始值一致。 - commit阶段,React创建input的DOM节点后,会将
inputRef.current赋值为该DOM节点,覆盖手动设置的null。 - 点击按钮时,current指向DOM节点,调用
focus()正常执行。
- 组件首次渲染时,函数体执行
二、手动修改ref.current未被覆盖的原因
React处理ref的核心规则:
- 首次渲染:仅当ref.current为初始null(或未被篡改为非DOM值)时,React才会在创建DOM后将节点赋值给它;若手动设置了非预期值,部分版本会跳过自动赋值。
- 更新渲染:React会在更新DOM前把ref.current设为null,更新后再重新赋值DOM节点。此时无论之前手动修改过什么,都会被React的自动赋值覆盖——但代码1是首次渲染的特殊场景,不适用这个逻辑。
三、第三个示例的执行逻辑
<input ref={inputRef}>在每次重新渲染时的行为:
- 首次渲染:创建第一个input的DOM节点,赋值给
inputRef.current。 - 第一次点击按钮:
- 调用
inputRef.current.focus(),聚焦第一个输入框。 - 手动把
inputRef.current设为inputRef2.current(第二个输入框的DOM节点)。 - 调用
setState触发重新渲染。
- 调用
- 重新渲染阶段:
useRef返回原有的ref对象(其引用在组件生命周期内保持不变),函数体执行时不会重置ref.current。 - commit阶段:React检查第一个input的DOM是否有变化——由于DOM未被卸载或重建,React不会自动更新
inputRef.current(仅当DOM被创建/卸载时,React才会主动修改ref.current)。 - 第二次点击按钮:
inputRef.current仍指向第二个输入框的DOM节点,调用focus()会聚焦第二个输入框。
总结:React仅在DOM节点被创建(首次渲染)或卸载时自动更新ref.current;若DOM节点持续存在且未重建,即使组件重新渲染,React也不会覆盖开发者手动修改的ref.current。
内容的提问来源于stack exchange,提问作者Philip Zhu

