一、问题背景与核心结论:理清 refs 与 render 的关系
1.1 问题背景:在 React中,是否可以在render 方法中访问refs?为什么?
在日常 React 开发中,我们经常需要通过 refs 直接操作 DOM 或子组件实例。一个常见的疑问随之而来:在 React中,是否可以在render 方法中访问refs?为什么? 这个问题看似简单,却涉及到 React 渲染机制、生命周期以及函数式编程理念的核心。许多初学者在 render 方法中尝试通过 this.refs.xxx 或 ref.current 来读取节点信息,结果却遭遇 null 或与预期不一致的值,甚至引发难以排查的 bug。本文将系统性地回答这一问题,并给出可落地的正确写法。
1.2 核心结论:不建议且不应当在 render 中访问 refs
结论先行:不应当在 render 方法中访问 refs。React 官方文档也明确建议避免在 render 期间访问 refs。原因可以归纳为三点:
- refs 在组件挂载完成后(commit 阶段)才会被赋值,render 执行时 refs 可能尚未就绪;
- render 应当是纯函数,访问 refs 会引入副作用,破坏可预测性;
- 在 render 中读取 refs 容易拿到过时或错误的数据,造成渲染不一致。
1.3 Refs 的基本概念:Refs 是什么、何时被赋值
Refs 是 React 提供的一种 "逃生舱" 机制,用于访问 DOM 节点或类组件实例。常见的创建方式有三种:
React.createRef():类组件中常用,返回一个{ current: null }对象;- 回调 Refs:
ref={el => this.myRef = el},在挂载和卸载时被调用; useRefHook:函数组件中使用,返回一个可变的 ref 对象。
关键点在于:ref 对象的 current 属性是在 commit 阶段才被赋值的,而非 render 阶段。这是理解 "为何不能在 render 中访问 refs" 的基石。
二、为什么不能在 render 中访问 refs:从机制到原理
2.1 Refs 的赋值时机:render 阶段 refs 尚未就绪
React 的工作流程可以简化为两个阶段:Render 阶段(协调)与 Commit 阶段(提交)。在 Render 阶段,React 会计算新的虚拟 DOM 树,但此时真实的 DOM 尚未更新,refs 也尚未被赋值。只有进入 Commit 阶段,React 才会完成 DOM 的更新并把节点实例挂到 ref 上。因此在 render 方法执行时,ref.current 通常还是 null,或者指向 "上一次" 的旧节点。
2.2 render 应当保持纯函数特性:访问 refs 破坏纯度
React 设计哲学强调 render 应当是一个纯函数:相同的 props 和 state 应当产生相同的输出。如果我们在 render 中读取 refs,就相当于让渲染结果依赖于 "组件外部的可变状态" 或 "上一次渲染的 DOM 状态",这会让组件变得难以预测、难以测试,也容易在 React 的并发模式(Concurrent Mode)下引发难以复现的 bug。访问 refs、修改 refs、触发网络请求等都属于副作用,应当放在 componentDidMount、componentDidUpdate 或 useEffect 中。
2.3 时机错乱导致的问题:拿到的可能是过时的数据
即便在某些场景下 refs 已经被赋值(比如组件已经挂载,进行再次 render),在 render 中读取 refs 仍存在风险。因为此时 DOM 还没有更新到本次 render 对应的状态,refs 指向的是 "上一次提交" 的节点。例如你想根据某个输入框的当前宽度来决定布局,但在 render 中读取到的宽度其实是上一次的值,会导致界面 "慢一拍" 甚至抖动。
2.4 流程图解析:render 与 refs 赋值的时间线
下面通过 Mermaid 流程图直观展示 React 渲染流程中 refs 被赋值的时机:
从图中可以清楚看到:render 阶段访问 refs 是 "抢跑",而真正的安全时机是 Commit 阶段之后。
三、正确的使用方式:在合适的时机访问 refs
3.1 在生命周期中访问 refs:类组件的最佳实践
在类组件中,应当在 componentDidMount 与 componentDidUpdate 中访问 refs。这两个生命周期都在 Commit 阶段之后执行,此时 DOM 已经更新完毕,refs 已经指向最新的节点。例如,要在挂载后聚焦一个输入框:
class AutoFocusInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
// 正确:在 commit 之后访问 refs
this.inputRef.current.focus();
}
render() {
// 错误示范:不要在这里访问 this.inputRef.current
return <input ref={this.inputRef} type="text" />;
}
}
如果在 render 中读取 this.inputRef.current,首次渲染时它必然是 null,没有任何意义。
3.2 函数组件中使用 useRef 与 useEffect:Hook 时代的写法
函数组件没有生命周期,但可以通过 useEffect 来模拟 "挂载后访问 refs" 的时机。useEffect 的回调在 DOM 更新之后执行,是访问 refs 的安全位置:
import React, { useRef, useEffect } from 'react';
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// 正确:在 effect 中访问 ref
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
需要注意:不要在组件主体(相当于 render)中直接读写 inputRef.current,这等同于在 render 中访问 refs,存在同样的风险。
3.3 回调 Refs 与 createRef 的对比:灵活性与时机的取舍
除了 createRef 与 useRef,React 还提供 "回调 Refs" 的形式。回调 Refs 会在节点挂载时以 DOM 作为参数调用,在卸载时以 null 调用。它可以让你在节点就绪的瞬间执行逻辑,但要避免在回调中触发会导致再次渲染的状态更新:
function MeasureInput() {
const measureRef = (node) => {
if (node !== null) {
// node 已就绪,可在此读取尺寸等信息
console.log('width:', node.getBoundingClientRect().width);
}
};
return <input ref={measureRef} type="text" />;
}
回调 Refs 适合 "需要根据节点信息立即做一些不涉及渲染的逻辑" 的场景,但若需要更新状态,仍应通过 useEffect 来触发,避免在 render 链路中造成循环。
3.4 最佳实践总结:把 refs 访问放到 commit 之后
综合以上分析,可以提炼出几条最佳实践:
- 始终将 refs 的访问放在 Commit 阶段之后,即类组件的
componentDidMount/componentDidUpdate,或函数组件的useEffect; - 保持 render 纯净,不要在 render 中读取或修改 refs,也不要在此触发副作用;
- 如果确实需要 "在节点挂载时立即执行某段逻辑",优先使用回调 Refs,但避免在其中触发会引发重渲染的更新;
- 当你发现 "必须在 render 中读取 refs 才能渲染正确" 时,通常说明状态设计有问题,应当把相关信息提取为 state,让数据驱动渲染,而不是让 DOM 反向驱动渲染。
回到最初的问题——在 React中,是否可以在render 方法中访问refs?为什么? 答案已经清晰:技术上或许能拿到值(在非首次渲染时),但从机制、理念与稳定性三个维度来看都不应当这么做。把 refs 当作 "提交后的产物",让 render 保持纯粹,才能写出可预测、易维护的 React 代码。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_41840843/article/details/163512156




