前言
对于初学react
的同学,不知道ref
会不会让你感觉头大,下面我分别从ref
的概念、class
组件、函数式组件,以及ref在项目中使用的场景来对ref
进行介绍;希望对大家有帮助!
ref是什么?
在react
早期版本中ref
是这样定义的(react.yubolun.com/docs/refs-a…
refs提供了一种方式,用于访问在render方法中创建的DOM节点或React元素
ref的创建与使用
在class
组件中提供了三种创建ref
的方式
旧版API: String Refs (已过时)
class App extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
setTimeout(() => {
// 2. 通过 this.refs.xxx 获取 DOM 节点
this.refs.textInput.value = "new value";
}, 2000);
console.log(this.refs.textInput);
}
render() {
// 1. ref 直接传入一个字符串
return (<input ref="textInput" value="value" />);
}
}
打印结果:获取的是input元素
String类型的refs存在问题,它已过时并可能会在未来的版本被移除。如果你目前还在使用this.refs.xxx这种方式访问refs,建议用其他两种方式替代。
回调refs
class App extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
setTimeout(() => {
// 2. 通过实例属性获取 DOM 节点
this.textInput.value = "new value";
}, 2000);
console.log(" this.textInput", this.textInput);
}
render() {
// 1. ref 传入一个回调函数
// 该函数中接受 React 组件实例或 DOM 元素作为参数
// 我们通常会将其存储到具体的实例属性(this.textInput)
return (
<input
ref={(element) => {
this.textInput = element;
}}
value="value"
/>
);
}
}
createRef
class App extends React.Component {
constructor(props) {
super(props);
// 1. 使用 createRef 创建 Refs
// 并将 Refs 分配给实例属性 textInputRef,以便在整个组件中引用
this.textInputRef = React.createRef();
}
componentDidMount() {
setTimeout(() => {
// 3. 通过 Refs 的 current 属性进行引用
this.textInputRef.current.value = "new value";
}, 2000);
console.log("this.textInputRef", this.textInputRef);
}
render() {
// 2. 通过 ref 属性附加到 React 元素
return (<input ref={this.textInputRef} value="value" />);
}
}
打印结果:
ref的值取决于节点的类型:
- 当
ref
属性被用于一个普通的 HTML 元素时,React.createRef()
将接收底层 DOM 元素作为它的current
属性以创建ref
。 - 当
ref
属性被用于一个自定义类组件时,ref
对象将接收该组件已挂载的实例作为它的current
。 - 你不能在函数式组件上使用
ref
属性,因为它们没有实例。
⭐️⭐️ useRef
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
开始
</button>
<button onClick={handleStop}>
停止
</button>
</>
);
}
该例子中ref
存储着interval ID
,用来记录你需要取消现有的 interval
我们可以看出ref
不仅可以存储DOM
元素( DOM接收ref)、实例(类组件接收ref)还可以存储数据,其实我们可以用ref存储任何数据更多使用技巧可以查看 zh-hans.react.dev/learn/refer…
forwardRef的作用
ref使用在函数组件上
以上四种创建ref
的方式,都不可以直接作用到函数组件上,如果想获取到函数组件的ref
我们可以使用forwardRef
。
例如:
import { useRef } from 'react';
function MyInput(props) {
return <input {...props} />;
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
此时控制台输出
发生这种情况是因为默认情况下,
React
不允许组件访问其他组件的DOM节点。想要暴露其DOM
节点组件必须指定将它的ref
“转发”给一个子组件。
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
如果你使用 React 16.3 或更高, 这种情况下我们推荐使用 ref 转发。 Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。关于怎样对父组件暴露子组件的 DOM 节点,在 ref 转发文档 中有一个详细的例子。
如果你使用 React 16.2 或更低,或者你需要比 ref
转发更高的灵活性,你可以使用 这个替代方案 将 ref 作为特殊名字的 prop 直接传递。
useImperativeHandle
使用命令句柄暴露一部分API
在上面的例子中,MyInput
暴露了原始的DOM
元素input
。这让父组件可以对其调用focus()
方法。然而,这也让其父组件能够做其他事情---例如,改变css
样式。在一些不常见的情况下,你可能希望限制暴露的功能。你可以使useImperativeHandle
import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}
ref在高阶组件中的使用
import React from "react";
function HocWithRef(WrappedComponent) {
return React.forwardRef((props, ref) => {
// 方式一:可以将ref当作普通的props传递
return <WrappedComponent {...props} ref={props.testRef} />;
// 方式二:用forwardRef第二个参数正常接收ref
// return <WrappedComponent {...props} ref={ref} />;
});
}
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
age: 18
};
}
render() {
return <div>这是一个组件</div>;
}
}
const MyComponentWithRef = HocWithRef(MyComponent);
class App extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
componentDidMount() {
// 通过ref访问或操作MyComponent组件
console.log(this.myRef.current)
}
render() {
// 方式一:和上面获取方式一的对应 将ref当作props传递
return <MyComponentWithRef testRef={this.myRef} />;
// 方式二:和上面获取方式二的对应 直接传递ref通过forwardRef接收ref
//return <MyComponentWithRef ref={this.myRef} />;
}
}
export default App;
补充
wrappedComponentRef
wrappedComponentRef
估计只会在维护react老版本项目中才会碰到,使用场景在antd v3 Form使用中3x.ant.design/components/… 详细内容可查看文档
总结
在react
全面拥抱hook
的时代,大家尽量用函数组件useRef
的方式来创建ref
,其他几种方式也大致了解,毕竟在维护项目的时候,可能会遇到;我理解ref
就像一个存储库一样,我们可以用它存储我们想要的数据,并且它还可以在组件中穿梭,让我们可以轻松的在父组件中使用子组件中的方法(实例化)等数据,可以让我们获取到DOM
,操作DOM
(react
并不推荐我们直接操作DOM
,但一些时候它又是需要的)想要更加透彻的了解ref
再次推荐zh-hans.react.dev/reference/r… 相关章节。
(有问题欢迎指正,一切以官方文档为准哈~)