React Ref引用机制解析
React Ref引用机制
- 一、核心作用
- 二、创建 Ref 的三种方式
- 三、Ref 的类型与适用场景
- 四、Ref 转发(Forward Ref)
- 五、注意事项与最佳实践
在 React 中,
ref
是用于访问
DOM 元素或
组件实例的引用机制。它允许我们突破 React 的声明式抽象,直接操作底层实例,是组件与 DOM/子组件交互的重要桥梁。以下从基础用法、类型、应用场景到原理深入,全面解析
ref
:
一、核心作用
- 访问 DOM 元素:获取输入框、按钮等 DOM 节点,操作其属性(如聚焦、滚动)。
- 访问组件实例:调用子组件暴露的方法或访问其内部状态(类组件场景)。
- 跨渲染周期保存值:在函数组件中存储不触发重新渲染的可变值(如定时器、上一次的状态)。
二、创建 Ref 的三种方式
(1) 类组件:createRef(官方推荐):生成一个 { current: null }
的对象,挂载后 current
指向目标元素/实例,类组件中通过 this.refName.current
访问。
class InputComponent extends React.Component {inputRef = React.createRef();//ref创建方式getContent = () => {console.log("获取输入框中的值:",this.inputRef.current.value);};render() {return (<div><input type="text" ref={this.inputRef} /><button onClick={this.getContent}>获取内容</button></div>);}
}
ReactDOM.render(<InputComponent />, document.getElementById("box"));
(2) 函数组件:useRef
(Hook 专属):返回一个 持久化的可变对象,组件重新渲染时 inputRef
引用不变,仅 current
值更新。
function InputHook() {const inputRef = useRef(null);const getContent = () => {const value = inputRef.current.value;console.log("获取输入框中的值:", value);};return (<div className="input-container"><input type="text" ref={inputRef} placeholder="请输入内容"/><button onClick={getContent}>获取内容</button></div>);
}
(3) 回调 Ref(函数形式,更灵活):通过 ref={(el) => { /* 保存引用 */ }}
形式绑定,el
为目标元素(挂载时为实例,卸载时为 null
),支持更精细的控制(如延迟绑定、条件绑定)。
// 类组件或函数组件均可使用
class CallbackRefDemo extends React.Component {// 类组件中定义回调函数setInputRef = (input) => {this.input = input; // 直接保存 DOM 引用return () => { this.input = null; }; // 组件卸载时清理(避免内存泄漏)};render() {return <input ref={this.setInputRef} />;}
}
// 函数组件中使用箭头函数
function FunctionCallbackRef() {let input = null; // 直接使用变量(需配合 useEffect 清理)const setRef = (el) => { input = el; };return <input ref={setRef} />;
}
如果组件卸载后仍保留对 DOM 元素的引用,这些引用会一直存在于内存中,无法被垃圾回收器回收,导致内存泄漏。卸载后访问已清理的引用(如 this.input.focus())会报错,清理确保引用为 null,避免运行时错误。
三、Ref 的类型与适用场景
(1)DOM Ref:操作原生 DOM
- 典型场景:
- 表单输入(获取值、聚焦):
// 类组件 this.inputRef.current.value = "新值"; // 函数组件 inputRef.current.select(); // 全选输入框内容
- 动画与DOM操作(滚动、尺寸测量):
useLayoutEffect(() => {const height = containerRef.current.offsetHeight;// 根据高度调整样式 }, [containerRef]);
- 表单输入(获取值、聚焦):
补充说明,这里 useLayoutEffect 在 DOM 更新后、浏览器绘制前执行,确保 setHeight 的状态更新在同一渲染周期内完成,避免因异步更新导致的布局闪烁(如先渲染旧高度,再渲染新高度)。
(2) 组件 Ref:访问子组件实例(类组件)
- 前提:子组件必须是 类组件(函数组件无实例),且通过
ref
暴露方法。// 子组件(类组件) class ChildComponent extends React.Component {getValue = () => "子组件数据";render() { return <div>子组件</div>; } }// 父组件调用子组件方法 class ParentComponent extends React.Component {childRef = React.createRef();handleClick = () => {const data = this.childRef.current.getValue(); // 调用子组件方法console.log(data); // 输出 "子组件数据"};render() {return (<ChildComponent ref={this.childRef} /><button onClick={this.handleClick}>获取子组件数据</button>);} }
(3)跨渲染周期存储值(非 DOM 场景)
- 场景:保存上一次的状态、定时器 ID、临时数据(不触发重新渲染)。
- 示例:记录上一次的 props 值
function Component({ value }) {const prevValue = useRef(); // 初始值为 undefineduseEffect(() => {console.log("当前值:", value, "上一次值:", prevValue.current);prevValue.current = value; // 更新上一次的值}, [value]);return <div>{value}</div>; }
四、Ref 转发(Forward Ref)
将父组件的 ref
传递给子组件,使其可访问子组件的 DOM 或实例(常用于高阶组件或自定义组件)。
(1)函数组件转发 Ref(需 React.forwardRef
),通俗点来说指的是在父组件中构建一个ref引用挂在子组件示例上。
// 子组件(函数组件,接收 ref)
const FancyInput = React.forwardRef((props, ref) => {return <input type="text" ref={ref} {...props} />;
});
function Parent() {// 父组件使用const inputRef = useRef();return (<FancyInput ref={inputRef} /><button onClick={() => inputRef.current.focus()}>聚焦</button>);
}
(2)类组件转发 Ref(需 ref
属性透传),和上面的一个意思,这里其实可以发现react的js逻辑会复杂一点。
class ChildClassComponent extends React.Component {render() { return <input ref={this.props.ref} />; } // 显式接收 ref
}
<ChildClassComponent ref={inputRef} /> // 父组件绑定 ref
五、注意事项与最佳实践
(1)避免滥用 Ref,优先使用状态(state
)和 props 驱动 UI,仅在以下场景使用 ref:
- 必须直接操作 DOM(如聚焦、滚动)。
- 访问类组件实例的方法(函数组件无实例,需用自定义 Hook 替代)。
- 跨渲染周期保存值(如
useRef
存储定时器 ID)。
下面给出一个具体的实践案例:
function TimerComponent() {// 使用useRef创建一个引用对象来存储定时器的IDconst timerId = useRef(null);// 设置定时器的函数const setTimer = () => {timerId.current = setInterval(() => {console.log('Tick');}, 1000);};// 清除定时器的函数const clearTimer = () => {if (timerId.current) {clearInterval(timerId.current);timerId.current = null; // 清除引用并设置为null}};// 使用useEffect来启动和清除定时器useEffect(() => {setTimer();return () => {clearTimer(); // 组件卸载时清除定时器};}, []); // 空依赖数组意味着这个effect只运行一次,在组件挂载后和卸载前return (<div><h1>这是一个定时器示例</h1></div>);
}
在 React 函数组件里,每次渲染都会重新执行函数体,函数内部的变量会被重新初始化。这里不同渲染周期的触发节点包括:state变化、props变化、父组件重新渲染、forceUpdate强制刷新(一般用于直接操作dom或者使用了非React提供的状态管理库而强制更新UI)、订阅的Context变化,但有些情况下,我们需要在不同的渲染周期之间保存特定的值,定时器 ID 就是这样的值。useRef 返回的引用对象在组件的整个生命周期内保持不变,只有 current 属性的值可以被修改,而且修改它不会触发组件重新渲染,这使得它非常适合用来跨渲染周期保存定时器 ID。
结合前面提到的react的渲染机制,这里需要借助于ref和useEffect为代码启动一个定时器就很好理解了(保证这个定时器的工作不会受到组件渲染的影响),最后再调用clearTimer删除这个定时器,由于一开始的定时器引用被挂载到了ref对象上了,因此为了避免出现内存泄漏(程序终止后泄漏的内存会被系统回收,但是开发需要避免),最后需要清除这个定时器,这样做整个过程的性能较好。
(2)字符串 Ref(React 17+ 已移除支持),应当使用 createRef
或回调 ref;
(3) Ref 的更新时机,ref.current
在 组件挂载后可用(componentDidMount
或 useEffect
中),卸载时置为 null
;多次渲染时,若 ref 绑定的元素未变化,current
引用不变,因此可以借助ref避免不必要的对象创建、减少垃圾回收压力、提供稳定的数据引用减少 DOM 操作,有效提升了 React 应用的性能;
(4) 函数组件与类组件的差异
特性 | 类组件(createRef ) | 函数组件(useRef ) |
---|---|---|
引用创建方式 | 类属性(this.ref = createRef() ) | Hook(const ref = useRef() ) |
跨渲染周期持久性 | 实例属性,自然持久化 | Hook 返回的对象,自动持久化 |
访问子组件实例 | 支持(类组件) | 仅支持类组件(函数组件无子组件实例) |
自定义逻辑 | 通过回调 ref 实现 | 直接操作 ref.current |
这里两个概念单独补充说明:
a. 自然持久化:类组件基于 ES6 类的概念,在实例化时会创建一个实例对象,该对象的属性(包括状态 this.state 和其他自定义属性)在组件的整个生命周期内都存在于内存中,并且可以被直接访问和修改。这种持久化是基于类实例的自然特性,只要类实例没有被销毁(即组件没有卸载),其内部的状态和属性就会持续存在;
b. 自动持久化:通过使用 React Hooks(如 useState、useReducer、useRef 等),可以实现数据在跨渲染周期的自动持久化。这些 Hooks 由 React 框架来管理,它们会自动记住上一次渲染时的值,并在合适的时机更新和返回这些值,无需开发者手动去处理复杂的实例管理和属性保存逻辑。
(5)内存泄漏与清理
绑定在 DOM 上的 ref 会自动清理(组件卸载时 current
置为 null
),但手动创建的非 DOM 引用(如定时器)需在 useEffect
中清理,useEffect
当组件第一次渲染到 DOM 后,useEffect 里的函数会被执行。之后在组件的每次更新(状态或 props 改变)后,useEffect 里的函数也会再次执行。其实也就是在(1)中提到的场景。