前言
简单整理了一下React中ref、useRef和forwardRef(重点讲) 的使用。
useRef与Ref的区别
一个标准的Ref是一个对象:{current:null},用来绑定DOM元素的引用和组件的实例,在React16.8之前通常是用React.createRef来创建。
在生成Ref对象时,常用的有两种方法:在类组件中,我们往往使用React.createRef来生成Ref对象。而useRef通常用于函数组件中Ref对象的生成,这也是React.createRef与useRef在使用上的一个区别。
简单来说:
useRef只能够函数组件中使用React.createRef能够在类组件和函数组件中使用
Ref作用
- 组件内使用
ref,获取dom元素 - 在类组件中,
ref作为子组件的属性,获取的是该子组件的实例
组件内使用ref,获取dom元素
// class组件
import React, { Component } from 'react';
class Child extends Component {
divRef = React.createRef();
componentDidMount() {
console.log(this.divRef.current);
}
render() {
return (
<div ref={this.divRef}>dom元素</div>
)
}
}
export default Child;
// 函数式组件
import React, { Component, useEffect, useRef } from 'react';
const Child = () => {
const divRef = useRef();
useEffect(()=>{
console.log(divRef.current);
},[])
return (
<div ref={divRef}>dom元素</div>
)
}
export default Child;
ref作为子组件的属性,获取的是该子组件
这种情况只适用于类组件,因为函数组件是没有状态的(hooks之前);
import React, { Component } from 'react';
class Child extends Component {
test(value) {
console.log(value);
}
componentDidMount() { }
render() {
return (<div>子组件</div>)
}
}
class Parent extends Component {
childRef = React.createRef();
componentDidMount() {
console.log(this.childRef.current.test("获取child的test"));
}
render() {
return <Child ref={this.childRef} />
}
}
export default Parent;
获取子组件中的dom的话
import React, { Component } from 'react';
class Child extends Component {
componentDidMount() {
console.log(this.props);
}
render() {
return (<div ref={this.props.myRef}>子组件</div>)
}
}
class Parent extends Component {
childRef = React.createRef();
componentDidMount() {
console.log(this.childRef.current);// 子组件的dom
}
render() {
return <Child myRef={this.childRef} />
}
}
export default Parent;
函数组件需要用到forwardRef,接下来会讲。
forwardRef作用
它是react16新增的方法,返回react组件,两个非常有用的场景来自官网:
- 转发refs到DOM组件
- 在高阶组件中转发refs
使用方式:
import React, { useState, useEffect, useRef } from 'react';
const Child = React.forwardRef((props,ref)=>{
return <input ref={ref}></input>
})
const Parent = ()=>{
const childrenRef = useRef();
useEffect(()=>{
console.log(childrenRef.current); // child input
},[])
return <Child ref={childrenRef}></Child>
}
export default Parent
仔细一看,怎么和上面获取子组件中的dom 区别不大。为什么还需要用forwardRef去包装一下呢?还有为什么ref不能作为组件属性注入到props中?
<Child myRef={this.childRef} />
官网是这样描述:Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。
什么意思呢?就是ref一般是不会用到的,也让开发者不要滥用。
这是因为使用ref会脱离React的控制。
比如:DOM聚焦 需要调用input.focus(),直接执行DOM API是不受React控制的。但为了保证应用的健壮,React也要尽可能防止他们失控。
失控的Ref
首先来看不失控的情况:
- 执行
ref.current的focus、blur等方法 - 执行
ref.current.scrollIntoView使element滚动到视野内 - 执行
ref.current.getBoundingClientRect测量DOM尺寸
那失控的情况呢?
- 执行
ref.current.remove移除DOM - 执行
ref.current.appendChild插入子节点
通过ref执行这些操作就属于失控的情况。看下面代码:
import React,{useState,useRef} from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => { setShow(!show) }}>
click
</button>
<button
onClick={() => {
ref.current.remove();
}}>
移除dom
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
click:是通过React控制的方式移除P节点。
移除dom:直接操作DOM移除P节点。
如果先点击click,再点击移除dom,则会报错:
这是使用使用Ref操作DOM造成的失控情况
如何限制失控
现在问题来了,既然叫失控了,那就是React没法控制的(React总不能限制开发者不能使用DOM API吧?),那如何限制失控呢?正常情况下,基于dom封装的组件是可以直接把ref指向dom:
function MyInput(props) {
const ref = useRef(null);
return <input ref={ref} {...props} />;
}
但是MyInput如果作为子组件,内部就不能直接获取ref:
import React, { useRef } from 'react';
function MyInput(props) {
return <input {...props} />;
}
export default () => {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
input聚焦
</button>
</>
);
}
现在inputRef是拿不到MyInput中input的引用。
究其原因,就是上面说的为了将ref失控的范围控制在单个组件内,React默认情况下不支持跨组件传递ref。
人为取消限制
如果一定要取消这个限制,可以使用forwardRef API显式传递ref。如上面例子所示。
使用forwardRef(forward在这里是传递的意思)后,就能跨组件传递ref。
在例子中,我们将inputRef从Form跨组件传递到MyInput中,并与input产生关联。
在实践中,一些同学可能觉得forwardRef这一API有些多此一举。
但从ref失控的角度看,forwardRef的意图就很明显了:既然开发者手动调用forwardRef破除防止ref失控的限制,那他应该知道自己在做什么,也应该自己承担相应的风险。
ref其他作用
ref除了上面的作用以外,还可以用来缓存值,在某些场景下非常有用,比如:当进入页面 5s 后,输出当前最新的 num,代码如下:
import React, { useState, useEffect } from 'react';
const Test = ()=> {
const [num, setNum] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
console.log(num)
}, 5000);
return () => {
clearTimeout(timer);
}
}, [])
return <button onClick={() => setNum(c => c + 1)}>
click
</button>
}
export default Test
以上代码,实现了初始化 5s 后,输出 num。但是因为闭包问题,num始终是之前的值:0。就算多点击click,最终也是输出:0
仔细想想,有其他办法可以解决吗?目前只有用ref来缓存了。
import React, { useState, useEffect, useRef } from 'react';
const Test = () => {
const [num, setNum] = useState(0);
const ref = useRef(num);
ref.current = num;
useEffect(() => {
const timer = setTimeout(() => {
console.log(ref.current)
}, 5000);
return () => {
clearTimeout(timer);
}
}, [])
return <button onClick={() => setNum(c => c + 1)}>
click
</button>
}
export default Test
总结!
不想总结了~~~。