React中的ref

642 阅读7分钟

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

0.Refs 使用场景:

在某些情况下,我们需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素,例如:

  • 管理焦点,文本选择或媒体播放。

  • 触发强制动画。

  • 集成第三方 DOM 库。

1.createRef:

支持 在函数组件 和 类组件 内部使用

createRef 是 React16.3 版本中引入的。

1.1创建 Refs:

使用 React.createRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。通常在构造函数中,将 Refs 分配给实例属性,以便在整个组件中引用。

1.2访问 Refs:

ref 被传递给 render 中的元素时,对该节点的引用可以在 refcurrent 属性中访问。

import React from 'react';

export default class MyInput extends React.Component {
    constructor(props) {
        super(props);
        //分配给实例属性
        this.inputRef = React.createRef(null);
    }

    componentDidMount() {
        //通过 this.inputRef.current 获取对该节点的引用
        this.inputRef && this.inputRef.current.focus();
    }

    render() {
        //把 <input> ref 关联到构造函数中创建的 `inputRef` 上
        return (
            <input type="text" ref={this.inputRef}/>
        )
    }
}

🌵**ref 的值根据节点的类型而有所不同:**

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。

  • ref 属性用于自定义的 class 组件时, ref 对象接收组件的挂载实例作为其 current 属性。

  • 不能在函数组件上使用 ref 属性,因为函数组件没有实例。

🔮总结:refdom上,就获取dom节点的引用;挂组件上就获取该组件的实例。

  • 为 DOM 添加 ref,那么我们就可以通过 ref 获取到对该DOM节点的引用。

  • 而给React组件添加 ref,那么我们可以通过 ref 获取到该组件的实例【不能在函数组件上使用 ref 属性,因为函数组件没有实例】。

2.useRef:

🌟仅限于在函数组件内使用

useRef 是 React16.8 中引入的,只能在函数组件中使用。

2.1创建 Refs:

使用 React.useRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。

const refContainer = useRef(initialValue);

🌵useRef 返回的 ref 对象在组件的整个生命周期内保持不变

2.2访问 Refs:

ref 被传递给 React 元素时,对该节点的引用可以在 refcurrent 属性中访问。

import React from 'react';

export default function MyInput(props) {
  
    const inputRef = React.useRef(null);
  
    React.useEffect(() => {
        inputRef.current.focus();
    });
  
    return (
        <input type="text" ref={inputRef} />
    )
}

描述

  • 上述代码实现了,当MyInput组件加载完毕的时候,聚焦焦点事件。

灰常重要的知识点👇

🍊关于 React.useRef() 返回的 ref 对象在组件的整个生命周期内保持不变,我们来和 React.createRef() 来做一个对比,下面来通过代码来立即下:

import React, { useRef, useEffect, createRef, useState } from 'react';

function Myinput(){
    let[count,setCount]=useState(0);

    const cRef=createRef(null);
    const uRef=useRef(null);

    //通过[]让useEffect只执行一次
    useEffect(()=>{
        uRef.current.focus();
        window.cRef=cRef;
        window.uRef=uRef;
    },[]);

    useEffect(() => {
        //除了第一次为true, 其它每次都是 false 【createRef】
        console.log('cRef === window.cRef', cRef === window.cRef);
        console.log(cRef);
        //始终为true 【useRef】
        console.log('uRef === window.uRef', uRef === window.uRef);
        console.log(uRef);
    })

    return (
        <>
            <input type="text" ref={uRef}/>
            <input type="text" ref={cRef}/>
            <button onClick={()=>{setCount(count+1)}}>{count}</button>
        </>
    )
}
export default Myinput;

解释

  • useRef不管组件怎么更新始终是一个引用
  • createRef不是,每次生成换一个引用
  • useRef 返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用 React.createRef ,每次重新渲染组件都会重新创建 ref

2.3回调 Refs:

支持在函数组件和类组件内部使用

React 支持 回调 refs 的方式设置 Refs。这种方式可以帮助我们更精细的控制何时 Refs 被设置和解除。

**使用 回调 refs 需要将回调函数传递给 React元素ref 属性。**这个函数接受 React 组件实例 或 HTML DOM 元素作为参数,将其挂载到实例属性上,如下所示:

import React from 'react';

export default class MyInput extends React.Component {
    constructor(props) {
        super(props);
        this.inputRef = null;
      
        //ref回调
        this.setTextInputRef = (ele) => {
            this.inputRef = ele;
        }
    }

    componentDidMount() {
        this.inputRef && this.inputRef.focus();
    }
    render() {
        return (
            <input type="text" ref={this.setTextInputRef}/>
        )
    }
}

🌵挂载和卸载的两种情况:

  • 组件挂载:React 会在组件挂载时,调用 ref 回调函数并传入 DOM元素(或React实例);

  • 组件卸载:当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 Refs 一定是最新的。

可以在组件间传递回调形式的 refs.👇(组件间是可以把ref当做props来传递)

import React from 'react';

//父组件
export default function Form() {
  
    let ref = null;
    React.useEffect(() => {
        //ref 即是 MyInput 中的 input 节点
        ref.focus();
    }, [ref]);

    return (
        <>
            <MyInput inputRef={ele => ref = ele} />
            {/** other code */}
        </>
    )
}

//子组件:
function MyInput (props) {
    return (
        <input type="text" ref={props.inputRef}/>
    )
}

3.Ref的传递:

Hooks 之前,高阶组件(HOC) 和 render props 是 React 中复用组件逻辑的主要手段。

🌵尽管高阶组件的约定是将所有的 props 传递给被包装组件,但是 refs 是不会被传递的,事实上, ref 并不是一个 prop,和 key 一样,它由 React 专门处理。

这个问题可以通过 React.forwardRef (React 16.3中新增)来解决。

React.forwardRef 之前,这个问题,我们可以通过给容器组件添加 forwardedRef (prop的名字自行确定,不过不能是 ref 或者是 key).

🚀 React.forwardRef之前:

import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';

const withData = (WrappedComponent) => {
    class ProxyComponent extends React.Component {
        componentDidMount() {
            //code
        }
        //这里有个注意点就是使用时,我们需要知道这个组件是被包装之后的组件
        //将ref值传递给 forwardedRef 的 prop
        render() {
            const {forwardedRef, ...remainingProps} = this.props;
            return (
                <WrappedComponent ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
  
    //指定 displayName.   未复制静态方法(重点不是为了讲 HOC)
    ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    //复制非 React 静态方法
    hoistNonReactStatic(ProxyComponent, WrappedComponent);
    return ProxyComponent;
}

这个示例中,我们将 ref 的属性值通过 forwardedRefprop,传递给被包装的组件,使用:

class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}

MyInput = withData(MyInput);

function Form(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current)
    })
    //我们在使用 MyInput 时,需要区分其是否是包装过的组件,以确定是指定 ref 还是 forwardedRef
    return (
        <MyInput forwardedRef={inputRef} />
    )
}

有了React.forwardRef之后:

Ref 转发是一项将ref自动地通过组件传递到其一子组件的技巧,其允许某些组件接收 ref,并将其向下传递给子组件。

转发 ref 到DOM中:

import React from 'react';
//子组件
const MyInput = React.forwardRef((props, ref) => {
    return (
        <input type="text" ref={ref} {...props} />
    )
});

//父组件
function Form() {
  	//创建ref
    const inputRef = React.useRef(null);
  
    React.useEffect(() => {
        console.log(inputRef.current);//input节点
    })
    return (
        <MyInput ref={inputRef} />
    )
}

解释:

  • 调用 React.useRef 创建了一个 React ref 并将其赋值给 inputRef 变量。

  • 指定 ref 为JSX属性,并向下传递 MyInput ref={inputRef}

  • React 传递 refforwardRef 内函数 (props, ref) => ... 作为其第二个参数。

  • 向下转发该 ref 参数到 button ref={ref},将其指定为JSX属性

  • ref 挂载完成,inputRef.current 指向 input DOM节点

🍀注意:

第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref

4.useImperativeHandle的使用:

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值(说白了就是你想让父组件看到什么,可以进行通过这个useImperativeHandle进行设置)。

在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  
  const inputRef = useRef();
  
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  
  return <input ref={inputRef} ... />;
}

FancyInput = forwardRef(FancyInput);

用途:

  • useImperativeHandle可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛

  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

  • 父组件可以使用操作子组件中的多个 ref

例子👇

import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

//子组件:
function Child(props,parentRef){
    // 子组件内部自己创建 ref 
    let focusRef = useRef();
    let inputRef = useRef();
  
    useImperativeHandle(parentRef,()=>(
      // 这个函数会返回一个对象
      // 该对象会作为父组件 current 属性的值
      // 通过这种方式,父组件可以使用操作子组件中的多个 ref
        return {
            focusRef,
            inputRef,
            name:'计数器',
            focus(){
                focusRef.current.focus();
            },
            changeText(text){
                inputRef.current.value = text;
            }
        }
    });
    return (
        <>
            <input ref={focusRef}/>
            <input ref={inputRef}/>
        </>
    )

}
ForwardChild = forwardRef(Child);

//父组件:
function Parent(){
  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();
    // 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
    parentRef.current.addNumber(666);
    parentRef.current.changeText('<script>alert(1)</script>');
    console.log(parentRef.current.name);
  }
  return (
      <>
        <ForwardChild ref={parentRef}/>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

目前笔者总结这里以上几种场景吧,后续遇到其他新的使用场景会陆续再更新进来...