Ref 和 forwardRef
React 里的"逃生舱",直接操作 DOM
先说结论:Ref 就是绕开 React 的声明式数据流,直接拿到 DOM 节点或组件实例。
Ref 是什么?
正常情况 React 通过 props 控制 UI,Ref 让你可以直接访问底层 DOM:
function MyInput() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus(); // 直接调用 DOM 方法
};
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>聚焦</button>
</>
);
}
forwardRef 到底干了什么?
为什么函数组件不能直接用 ref?
首先得搞懂 ref 的本质:ref 是一个"容器",专门用来存 DOM 节点或组件实例的引用。
类组件可以有实例(因为是 new ClassName() 创建的),但函数组件只是一个函数执行过程,没有 this,没有实例。
// ❌ 函数组件没有实例,所以不能直接加 ref
function MyInput() {
return <input />;
}
<MyInput ref={inputRef} />; // 报错!
React 会报错:Function components cannot be given refs。
forwardRef 做了什么?
forwardRef 做的事情很简单:它让函数组件能接收到从父组件传进来的 ref,然后转发给内部的 DOM 节点。
const MyInput = forwardRef((props, ref) => {
// 这里有两个参数:
// props - 正常的 props
// ref - 父组件传进来的 ref
return <input ref={ref} />;
});
<MyInput ref={inputRef} />;
底层原理:
- 父组件
<MyInput ref={inputRef} /> 传了一个 ref 进来
forwardRef 把这个 ref 作为第二个参数传递给函数组件
- 函数组件内部把这个 ref 绑定到
<input ref={ref} />
- 最终
inputRef.current 指向了这个 input DOM 节点
说白了就是一层转发:父组件的 ref -> forwardRef 包装层 -> 传给内部 DOM。
useImperativeHandle 干了什么?
有时候我们不想把整个 DOM 暴露出去,只想让父组件调用几个特定方法:
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
reset: () => (inputRef.current.value = ""),
}));
return <input ref={inputRef} />;
});
底层原理:
useImperativeHandle 替换了原来的 ref 对象
- 父组件拿到的
ref.current 不再是 DOM 节点,而是你返回的这个对象
- 只能调用你暴露的方法,访问不到真实的 DOM
为什么这么做?
- 安全:不让父组件随意修改 DOM 的样式
- 封装:只暴露必要的接口,隐藏内部实现
createRef vs useRef
- createRef:每次渲染返回新对象,存不住数据(类组件用)
- useRef:整个生命周期保持同一引用,像类组件的
this.xxx
// ❌ createRef 在函数组件里每次都是新的
const ref = createRef();
// ✅ useRef 保持同一个引用
const ref = useRef(null);
总结
| 概念 | 作用 |
|---|
| ref | 存 DOM 节点或组件实例的引用 |
| forwardRef | 让函数组件能接收并转发 ref |
| useImperativeHandle | 替换 ref 为自定义对象,只暴露特定方法 |
| useRef | 在函数组件中保持引用持久化 |
记住:Ref 是"逃生舱",能用 props 解决的就别用 Ref。