🍉在React中,我们经常需要与组件内部的DOM元素进行交互。无论是触发一个动画、测量元素的尺寸,还是集成第三方的DOM操作库,我们都需要一种方法来获取对这些元素的引用。这就是ref
发挥作用的地方。
ref
是React提供的一种机制,用于访问DOM节点或组件实例。随着React Hooks的引入,我们得到了useRef
这个钩子,它为函数组件提供了一种声明性的方式来创建和使用refs。但React的ref
功能并不止步于此,它还包括了一些更高级的用法,比如createRef
、React.forwardRef
以及useImperativeHandle
。
在这篇文章中,我们将从基础的ref
概念开始,逐步深入到useRef
的使用,探索如何通过React.forwardRef
将ref
传递给子组件,以及如何使用useImperativeHandle
来处理一些需要直接操作DOM的场景。
😎下面正文开始~
首先,看官网使用 ref 引用值 – React 中文文档对ref的解释:
当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref 。
在官网可以看到,目前只推荐useRef,适用于函数组件。那么在一些老的项目中,创建refs是使用createRef
或者回调 Refs,这些过时的方法是用于class组件的。
类组件和createRef
示例
import { Component, createRef } from 'react';
export default class App extends Component {
constructor(props){
super();
this.inputRef = createRef();
console.log(this);
}
render() {
return (
<>
</>
);
}
}
先console.log(this)
查看输出的组件实例:
可以看到,组件实例上新增了一个 inputRef 属性来保存这个 Ref 对象,此时的inputRef.current
是null
。
把这个对象和input进行关联:
<input type="text" ref={this.inputRef}/>
此时,输出的this中,current就变成了input:
完整代码看一下inputRef对象的变化:
import { Component, createRef } from 'react';
export default class App extends Component {
constructor(props) {
super();
this.inputRef = createRef();
console.log(this.inputRef); // {current: null}
}
clickHandle = () => {
console.log(this.inputRef); // {current: input}
this.inputRef.current.focus();
}
render() {
return (
<>
<input type="text" ref={this.inputRef}/>
<button onClick={this.clickHandle}>focus</button>
</>
)
}
}
最后,可以用 ref.current
属性访问该 ref 的当前值,上面示例中,this.inputRef.current.focus()
即可聚焦input。
- 在函数组件中,你可能想要使用
useRef
,因为它始终返回相同的对象。 const ref = useRef()
等同于const [ref, _] = useState(() => createRef(null))
。
要点:
createRef
不接受任何参数。createRef
返回一个有current属性的对象,current初始值为null。
回调 Refs
回调Refs比createRef更早,来看一个示例:setRef
方法是一个回调函数,它接收一个参数 element
,并将其赋值给 this.inputRef
。在render方法中,将 setRef
函数作为 ref
属性传递给了 <input>
元素。
import React, { Component } from 'react';
export default class App extends Component {
constructor() {
super();
this.inputRef = null;
}
setRef = (element) => {
this.inputRef = element;
}
clickHandle = () => {
if(this.inputRef){
this.inputRef.focus();
}
}
render() {
return (
<>
<input type="text" ref={this.setRef} />
<button onClick={this.clickHandle}>focus</button>
</>
)
}
}
小结
以上的createRef和回调Refs的示例都是class组件,且在官方文档中,createRef
已被归类为过时的React API。现在推荐使用函数组件,在函数组件使用useRef。
函数组件和useRef
默认情况下,函数组件上为什么不能使用ref属性?
因为函数组件没有实例,但是在函数组件内部是可以使用ref的。
示例
使用函数组件和useRef实现上文示例:
import { useRef } from 'react';
export default function App() {
const inputRef = useRef();
const clickHandle = () => {
console.log(inputRef)
}
render() {
return (
<>
<input type="text" ref={inputRef} />
<button onClick={this.clickHandle}>focus</button>
</>
)
}
}
查看输出:
那么,useRef和createRef创建的ref有什么不同呢?
createRef也能在函数组件使用,在同一个函数组件中分别使用useRef和createRef创建的ref:
import React, { useRef, useState } from 'react';
export default function App() {
const [count, setCount] = useState(1);
let ref1 = useRef();
let ref2 = React.createRef();
console.log(ref1, '组件加载:useRef:ref1 ');
console.log(ref2, '组件加载:createRef:ref2 ');
const handleClick = () => {
console.log(ref1, '点击:useRef:ref1 ');
console.log(ref2, '点击:createRef:ref2 ');
setCount(count + 1); // state改变,组件重新渲染
}
return (
<>
<div>{count}</div>
<input type="text" ref={ref1} />
<input type="text" ref={ref2} />
<button onClick={handleClick}>
点击我!
</button>
</>
);
}
上图是从加载到点击触发handleClick的过程,从上图可以看到:
useRef创建的ref1是undefined -> input -> input 。
createRef创建的ref2是null -> input -> null 。
为什么会造成这样的差异呢?
因为函数组件每次重新渲染时,会重新执行这个函数,那么createRef()会重新执行,每次的ref1都是新建的。
useRef是挂在fiber上的,只会在第一次创建新的ref,类似于useState
就是因为在函数式组件中使用 createRef 创建 ref 时存在弊端,组件每次更新,ref 对象就会被重新创建,所以出现了 useRef 来解决这个问题。
官方文档中提到:
createRef
总是返回一个不同的对象。这相当于你自己编写了{ current: null }
。- 在函数组件中,你可能想要使用
useRef
,因为它始终返回相同的对象。const ref = useRef()
等同于const [ref, _] = useState(() => createRef(null))
。
useRef的其他用法
useRef 还接受一个初始值,可以用 ref.current
属性访问该 ref 的当前值。这个值可以读取也可以写入。
与 state 一样,React 会在每次重新渲染之间保留 ref。但是,设置 state 会重新渲染组件,更改 ref 不会重新渲染组件!利用这一特点,可以实现秒表。
该秒表需要用到定时器来更新计时的数据值,点击停止时,清除这个计时器。先看不使用useRef时,秒表有什么问题:
import { useState } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
let timer;
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
timer = setInterval(() => {
setNow(Date.now());
}, 10);
}
const handleEnd = () => {
clearInterval(timer);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>时间过去了: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>开始</button>
<button onClick={handleEnd}>停止</button>
</>
);
}
点击开始,正常计时。但是点击停止,秒表依然在计时。
因为组件重新渲染之后,这里的 timer 以及方法都会重新创建,timer 已经不是之前储存的interval ID了。
useRef 在组件的整个生命周期内都不会改变,此时就派上用场了!可以把timer存在useRef里:
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
let timer = useRef(null);
function handleStart() {
// 开始计时。
setStartTime(Date.now());
setNow(Date.now());
timer.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
const handleEnd = () <p align=right>=></p> {
clearInterval(timer.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={handleEnd}>
停止
</button>
</>
);
}
Ref 转发:React.forwardRef
Ref 转发是一个可选特性,指的是允许某些组件接收 ref,并将其向下传递给子组件,即转发
它给子组件。
先查看一个简单的示例:函数组件中,为child Component
绑定一个ref。
上文中提到:默认情况下,函数组件上为什么不能使用ref属性,因为函数组件没有实例,但是在函数组件内部是可以使用ref的。
所以这里的Child Component
我们使用class组件:
// child.jsx
class ChildCom extends React.Component {
constructor(props) {
super(props);
}
checkChild = () => {
console.log('Child Component的方法')
}
render() {
return <div>Child Component</div>
}
}
export default Child;
import ChildCom from "./Child";
import { useRef } from "react";
function App() {
const ChildRef = useRef();
function clickHandle() {
console.log(ChildRef, 'ChildRef.current---');
ChildRef.current.checkChild();
}
return (
<>
<Child ref={ChildRef} />
<button onClick={clickHandle}>click</button>
</>)
}
export default App;
在页面上点击按钮,查看输出:
可以看到,这样绑定ref是可以使用Child组件的方法的。
但是既然现在官方推荐都用函数组件了,真的不能用函数组件来写吗?
把Child换成函数组件试试,果然还是报错了:
此时,就到了React.forwardRef发挥作用的时候。我们把函数组件Child稍加改造:
const Child = (props, ref) => {
// 接收两个参数,第二个是ref,将其传递到要公开的 DOM 节点中
const checkChild = () => {
console.log('Child Component的方法')
}
return <h2 ref={ref}>Child Component forwardRef</h2>
}
// 用React.forwardRef包裹Child组件
export default React.forwardRef(Child);
点击,查看输出:
可以看到,APP组件中,我们可以得到Child暴露的DOM节点,但是Child组件的方法依然无法使用。此时就要用到useImperativeHandle来自定义要暴露给父组件的实例值了。
这个用作测试示例,可以自己输出玩玩
const Child = (props, ref) => {
const checkChild = () => {
console.log('Child Component的方法')
}
const handleChildClick = () => {
console.log('点击Child Component');
console.log(ref.current, 'Child Component =>=> ref.current');
}
return <div>
<h2 >Child Component Fun</h2>
<button onClick={handleChildClick}>点击Child</button>
<div ref={ref} >
<h5>标题</h5>
<input></input>
</div>
</div>
}
export default React.forwardRef(Child);
useImperativeHandle
useImperativeHandle – React 中文文档
useImperativeHandle
是 React 中的一个 Hook,它能让你自定义由ref 暴露出来的句柄。
useImperativeHandle一般和React.forwardRef配合使用,来自定义要暴露给父组件的实例值。
用刚才的例子写一下:
const Child = (props, ref) => {
// 自定义使用 ref 时向父组件要暴露的东西
// 第一个是parent组件传递过来的 ref
// 第二个参数是一个回调函数,该函数返回一个对象:
// 这个对象里定义映射关系,也就是具体要向parent组件暴露的东西
useImperativeHandle(ref, () => ({
// 暴露的方法名: Child组件内部的方法名 (😃可以不一致)
checkChild: checkChild,
}))
const checkChild = () => {
console.log('Child Component的方法')
}
return <h2 ref={ref}>forwardRef+useImperativeHandle</h2>
}
export default React.forwardRef(Child);
查看输出:
可以看到在APP组件可以用checkChild
方法了!
但是仔细看这个输出的ChildRef,里面只有checkChild方法,找不到Child暴露的DOM节点了😶
既然useImperativeHandle的回调函数能暴露多个方法,那么也通过暴露出去的方法得到子组件的DOM节点:
const Child = (props, ref) => {
// 组件内部创建ref
const h2Ref = useRef();
useImperativeHandle(ref, () => ({
checkChild: checkChild,
// 暴露一个printDOM方法
printDOM: () => {
console.log(h2Ref);
},
}))
const checkChild = () => {
console.log('Child Component的方法')
}
// 绑定ref
return <h2 ref={h2Ref}>forwardRef+useImperativeHandle</h2>
}
export default React.forwardRef(Child);
同时,在App.jsx
的clickHandle
方法中调用:
ChildRef.current.printDOM();
查看输出
可以看到ChildRef.current有两个方法,调用printDOM
也能得到h2Ref
绑定的DOM。