You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Ref绑定DOM的底层逻辑及异常场景解析

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.current during the commit. Before updating the DOM, React sets the affected ref.current values to null. After updating the DOM, React immediately sets them to the corresponding DOM nodes.

我理解为每次React更新时inputRef.current都会被重新赋值,但有两个疑问:

  1. 手动修改inputRef.current的操作为何未被<input ref={inputRef}/>覆盖?
  2. 为何赋值为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. 代码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. 代码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}>在每次重新渲染时的行为:

  1. 首次渲染:创建第一个input的DOM节点,赋值给inputRef.current。
  2. 第一次点击按钮:
    • 调用inputRef.current.focus(),聚焦第一个输入框。
    • 手动把inputRef.current设为inputRef2.current(第二个输入框的DOM节点)。
    • 调用setState触发重新渲染。
  3. 重新渲染阶段:useRef返回原有的ref对象(其引用在组件生命周期内保持不变),函数体执行时不会重置ref.current。
  4. commit阶段:React检查第一个input的DOM是否有变化——由于DOM未被卸载或重建,React不会自动更新inputRef.current(仅当DOM被创建/卸载时,React才会主动修改ref.current)。
  5. 第二次点击按钮:inputRef.current仍指向第二个输入框的DOM节点,调用focus()会聚焦第二个输入框。

总结:React仅在DOM节点被创建(首次渲染)或卸载时自动更新ref.current;若DOM节点持续存在且未重建,即使组件重新渲染,React也不会覆盖开发者手动修改的ref.current。


内容的提问来源于stack exchange,提问作者Philip Zhu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 17:34:51