React中的Ref

  React中ref是一个对象,它有一个current属性,可以对这个属性进行操作,用于获取DOM元素和保存变化的值。什么是保存变化的值?就是在组件中,你想保存与组件渲染无关的值,就是JSX中用不到的或不显示到页面上的值,比如setTimeout的返回的ID,就可以把这个值放到ref中。为什么要放到ref中,因为更改ref的值,不会引起组件的重新渲染,因为值与渲染无关,它也不应该引起组件渲染。怎么获取ref对象呢?调用useRef()函数和createRef()函数,它们返回ref对象。在组件的整个生命周期中,ref对象一直存在。组件创建,更准确地说法是,组件挂载,ref对象创建,组件销毁,ref对象销毁。

  useRef是一个React Hooks,在函数组件中使用,它还可以接受一个参数,用于初始化useRef返回的对象的current属性。

const ref = useRef(initialValue);

  使用useRef获取DOM元素,就是把ref对象赋给react element的ref属性, 每一个react element都有一个ref属性。组件挂载后,ref对象的current属性,就自动指向DOM元素

import React, { useRef } from "react";

const CustomTextInput = () => {
  const textInput = useRef();

  const focusTextInput = () => textInput.current.focus();

  return (
    <>
      <input type="text" ref={textInput} />
      <button onClick={focusTextInput}>Focus the text input</button>
    </>
  );
}

  组件挂载完成,textInput.current指向input输入框,就可以直接调用输入框的focus方法。   

  使用useRef保存变量的值,直接把变量或值,赋值给ref对象的current属性就可以了。

import React, { useRef, useEffect } from "react";

const Timer = () => {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      console.log("A second has passed");
    }, 1000);
    
    intervalRef.current = id;
    
    return () => clearInterval(intervalRef.current);
  });

  const handleCancel = () => clearInterval(intervalRef.current);
  
  return (
    <>
      //...
    </>
  );
}

  这里要注意的是更新ref对象的值,是一个side effect,因为这个值不参与渲染,更新值是React渲染之外,要做的事情,所以要放到useEffect或useLayoutEffect中,放到事件处理函数中也可以。如果以下有代码

import React, { useRef } from "react";

const RenderCounter = () => {
  const counter = useRef(0);
  counter.current = counter.current + 1;
  
  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};

   最好改成   

import React, { useRef } from "react";

const RenderCounter = () => {
  const counter = useRef(0);
  
  useEffect(() => {
    counter.current = counter.current + 1;
  }); 
  
  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};

  函数组件中也可以使用createRef, 但当使用createRef时,每一次组件渲染时都会创建全新的ref对象,而不是每一次渲染都共用一个ref对象,性能会有问题,再说useRef就是代替createRef的,所以在函数组件中就没有必要使用createRef了。

  其实,使用useRef,也可以获取到子组件,直接调用子组件中的方法,不过就是用点麻烦,因为ref只能获取到类组件的实例,也只有类才有实例。函数组件是没有实例的,怎么获取到它?使用forwardref, 把一个函数组件包起来,函数组件就多了一个ref属性。子组件中用useImperativeHandle暴露方法。结合forwardRef 和useImperativeHandle。使用create-react-app 创建React项目,在src中创建一个Counter组件

import React from 'react';
import { useState } from "react"

const Counter = () => {
    const [count, setCount] = useState(0);

    const clickHandler = () => {
        setCount(c => c + 1);
    }
    
    return (
        <p>count is {count} </p>
    )
}

export default Counter;

  然后在App.js中引入

import React from 'react';
import Counter from "./Counter";

function App() {
  return (
    <React.Fragment>
      <Counter></Counter>
      <button>Add</button>
    </React.Fragment>
  );
}

export default App;

  此时,如果想点击父组件App中的button来增加子组件的count,怎么办?首先,子组件Counter,要把clickHandler方法暴露出来。做法,1,export的不是组件了,而是forwardRef(组件); 2,组件要接受参数ref,const Counter = (props, ref); 3,  在组件内部,使用useImperativeHandle,它的第一个参数是ref,第二个参数是回调函数,返回一个对象,对象中的属性和方法,就可以在父组件中使用ref获取到。

import React, { forwardRef, useImperativeHandle } from 'react';
import { useState } from "react"

// 组件被forwardRef之后,组件多了一个ref属性
const Counter = (props, ref) => {
  const [count, setCount] = useState(0);

  const clickHandler = () => {
    setCount(c => c + 1);
  }
  // 第一个参数就是ref,暴露出click方法,供父组件使用
  useImperativeHandle(ref, () => {
    return ({
      click: clickHandler
    })
  })

  return (
    <p>count is {count} </p>
  )
}

// export forwardRef(组件)
export default forwardRef(Counter);

  其次,在父组件App中使用ref,引用子组件,并在button的click回调函数中使用ref

function App() {
  const counteRef = useRef();

  const handleClick = () => {
    counteRef.current.click();
  }

  return (
    <React.Fragment>
      <Counter ref={counteRef}></Counter>
      <button onClick={handleClick}>Add</button>
    </React.Fragment>
  );
}

  如果项目中使用了Redux和React-Redux,子组件export的是connect()(组件),是一个高阶组件,那父组件中怎么引用子组件呢?如果是react-redux 6.0以前的版本,connect函数第四个参数设置为{ withRef: true },父组件getWrappedInstance()就可以获取到包裹的子组件

connect(null, null, null, { withRef: true })(组件);

  如果是react-redux 6.0 以后的版本,使用 forwardRef: true , 代替{ withRef: true },父组件中的ref可以直接获取到包包裹的子组件

connect(null, null, null, { forwardRef: true })(组件);

   

posted @ 2022-03-09 12:20  SamWeb  阅读(4410)  评论(3编辑  收藏  举报