什么是 Ref?
允许用户直接访问 DOM 节点或 React 元素。
我们知道,在 React 中写的 jsx 都是虚拟 DOM,react-dom 库的 render 方法来负责将我们的 jsx 转换为真实 DOM,也就是说,开发者在写组件的时候,还没有生成真实 DOM 呢,那如果想拿到真实 DOM 怎么办?这就需要给组件添加 ref 了,可分为以下三种场景:
- 给原生组件添加 ref
- 给类组件添加 ref
- 给函数组件添加 ref
接下来分别进行介绍:
给原生组件添加 ref
如果给一个原生组件添加了 ref 属性,会把真实的 DOM 元素赋值给 ref.current,下面是一个具体的案例,给 input 输入框添加 ref,在 button 点击事件触发时,让 input 框获取焦点:
import React from 'react'
class Form extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
focus = () => {
this.ref.current.focus()
}
render() {
return (
<div>
<input ref={this.ref} />
<button onClick={this.focus}>获取焦点</button>
</div>
)
}
}
export default Form
代码非常浅显易懂,this.ref.current 其实就是指代了原生 input DOM 节点,实现的效果就是点击 button 之后触发 input 获取焦点:
给类组件添加 ref
如果我们自己写了一个组件,给它绑定 ref 会是什么效果呢?例如创建 Text 自定义组件,里面渲染了原生 input 元素:
import React from 'react'
class Text extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
focus = () => {
this.ref.current.focus()
}
render() {
return <input ref={this.ref} />
}
}
export default Text
我们在 Form 组件中,引入这个 Text 组件,给它绑定 ref:
import React from 'react'
import Text from './Text'
class Form extends React.Component {
constructor(props) {
super(props)
this.ref = React.createRef()
}
focus = () => {
this.ref.current.focus()
}
render() {
return (
<div>
<Text ref={this.ref} />
<button onClick={this.focus}>获取焦点</button>
</div>
)
}
}
export default Form
运行发现,效果和之前是一模一样的。其实给自定义组件绑定 ref 就相当于拿到了该组件的实例,有了实例就能调用它的任意方法了,所以上面的逻辑就是:
- Form 组件通过 ref 拿到 Text 组件的实例
- 点击按钮后,触发 Text 组件实例的 focus 方法
- Text 组件 focus 方法里面触发了 input 原生组件的 focus 方法
给函数组件添加 ref
函数组件跟类组件不同,它没有实例,所以当我们给自定义函数组件添加 ref 的时候,会报错:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
为了能够引用函数组件,我们必须通过 React.forwardRef 进行转发,代码如下:
import React from 'react'
function Text(props, forwardRef) {
return <input ref={forwardRef} />
}
export default React.forwardRef(Text)
当在 Form 组件给 Text 函数组件添加 ref 的时候,其实 ref 会被转发到 Text 函数组件的第二个参数里面,在函数组件内部,可以把这个 forwardRef 绑定到任意的组件上面,达到转发的效果。如果 Form 组件也改成函数组件的话,可以这么写:
import React from 'react'
import Text from './Text'
function Form() {
const ref = React.useRef()
return (
<div>
<Text ref={ref} />
<button onClick={() => ref.current.focus()}>获取焦点</button>
</div>
)
}
export default Form
由于 forwardRef 转发之后,把 Text 组件内部的元素直接暴露给外部调用方了,这样并不安全(类比类组件 ref 只能访问实例的方法),可以通过 useImperativeHandle 自定义向外暴露的对象,使用方法如下:
import React from 'react'
function Text(props, forwardRef) {
const ref = React.useRef()
React.useImperativeHandle(forwardRef, () => ({
focus: () => ref.current.focus()
}))
return <input ref={ref} />
}
export default React.forwardRef(Text)
useImperativeHandle 函数的第一个参数就是被转发的 ref,第二个参数是一个函数,返回值是用户自定义的想对外暴露的对象。