React.createRef、useRef、React.forwardRef与useImperativeHandle

🍉在React中,我们经常需要与组件内部的DOM元素进行交互。无论是触发一个动画、测量元素的尺寸,还是集成第三方的DOM操作库,我们都需要一种方法来获取对这些元素的引用。这就是ref发挥作用的地方。

ref是React提供的一种机制,用于访问DOM节点或组件实例。随着React Hooks的引入,我们得到了useRef这个钩子,它为函数组件提供了一种声明性的方式来创建和使用refs。但React的ref功能并不止步于此,它还包括了一些更高级的用法,比如createRefReact.forwardRef以及useImperativeHandle

在这篇文章中,我们将从基础的ref概念开始,逐步深入到useRef的使用,探索如何通过React.forwardRefref传递给子组件,以及如何使用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.currentnull

把这个对象和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

forwardRef – React 中文文档

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.jsxclickHandle方法中调用:

ChildRef.current.printDOM();

查看输出

在这里插入图片描述

可以看到ChildRef.current有两个方法,调用printDOM也能得到h2Ref绑定的DOM。

  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值