Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React知识总结(一)— Refs和DOM #55

Open
LightXJ opened this issue Jul 1, 2020 · 0 comments
Open

React知识总结(一)— Refs和DOM #55

LightXJ opened this issue Jul 1, 2020 · 0 comments

Comments

@LightXJ
Copy link
Owner

LightXJ commented Jul 1, 2020

前言

Refs 提供了一种访问在 render 方法中创建的 DOM 节点或 React 元素的方式。
在常规的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改子元素,你需要用新的 props 去重新渲染子元素。然而,在少数情况下,你需要在常规数据流外强制修改子元素。被修改的子元素可以是 React 组件实例,或者是一个 DOM 元素。在这种情况下,React 提供了解决办法。【可以通过refs获得子组件的实例或dom元素

何时使用Refs

下面有一些正好使用 refs 的场景:

  • 处理focus、文本选择或者媒体播放
  • 触发强制动画
  • 集成第三方DOM库
    如果可以通过声明式实现,就尽量避免使用refs。
    例如,相比于在Dialog组件中暴露open()和close()方法,最好传递isOpen属性。

不要过度使用Refs

你可能首先会想到在你的应用程序中使用refs来更新组件,如果是这种情况,请花一点时间,更多的关注在组件层中使用state。在组件层中,通常较高级别的 state 更为清晰。有关示例,请参考状态提升。

创建Refs的四种方式

  • 字符串模式:废弃不建议使用
  • 回调函数
  • React.createRef():React16.3提供
  • useRef:React16.8引入

createRef

支持在函数组件和类组件内部使用,createRef是React16.3版本中引入的。

创建Refs
使用React.createRef()创建Refs,并通过ref属性附加至React元素上。通常在构造函数中,将Refs分配给实例属性,以便在整个组件中引用。
访问Refs
当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中访问。

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 属性,因为函数组件没有实例。
    总结:为 DOM 添加 ref,那么我们就可以通过 ref 获取到对该DOM节点的引用。而给React组件添加 ref,那么我们可以通过 ref 获取到该组件的实例【不能在函数组件上使用 ref 属性,因为函数组件没有实例】。

useRef

仅限于函数组件内使用
useRef 是 React16.8 中引入的,只能在函数组件中使用。
创建Refs
使用 React.useRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。

const refContainer = useRef(initialValue);

useRef 返回的 ref 对象在组件的整个生命周期内保持不变。
访问Refs
当 ref 被传递给 React 元素时,对该节点的引用可以在 ref 的 current 属性中访问。

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} />
    )
}

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

import React, { useRef, useEffect, createRef, useState } from 'react';
function MyInput() {
    let [count, setCount] = useState(0);

    const myRef = createRef(null);
    const inputRef = useRef(null);
    //仅执行一次
    useEffect(() => {
        inputRef.current.focus();
        window.myRef = myRef;
        window.inputRef = inputRef;
    }, []);
    
    useEffect(() => {
        //除了第一次为true, 其它每次都是 false 【createRef】
        console.log('myRef === window.myRef', myRef === window.myRef);
        //始终为true 【useRef】
        console.log('inputRef === window.inputRef', inputRef === window.inputRef);
    })
    return (
        <>
            <input type="text" ref={inputRef}/>
            <button onClick={() => setCount(count+1)}>{count}</button>
        </>
    )
}

回调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;
        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。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 Refs 一定是最新的。

可以在组件间传递回调形式的 refs.

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}/>
    )
}

字符串 Refs(过时API)

函数组件内部不支持使用 字符串 refs [支持 createRef | useRef | 回调 Ref]

function MyInput() {
    return (
        <>
            <input type='text' ref={'inputRef'} />
        </>
    )
}

报错:
image

类组件
通过 this.refs.XXX 获取 React 元素。

class MyInput extends React.Component {
    componentDidMount() {
        this.refs.inputRef.focus();
    }
    render() {
        return (
            <input type='text' ref={'inputRef'} />
        )
    }
}

Ref传递

在 Hook 之前,高阶组件(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 的属性值通过 forwardedRef 的 prop,传递给被包装的组件,使用:

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} />
    )
}

运行结果:
image

补充知识:

静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

hoist-non-react-statics的作用
当你将HOC应用于组件时,原始组件将用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);

// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:

function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 必须准确知道应该拷贝哪些方法 :(
  Enhance.staticMethod = WrappedComponent.staticMethod;
  return Enhance;
}

但要这样做,你需要知道哪些方法应该被拷贝。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

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() {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current);//input节点
    })
    return (
        <MyInput ref={inputRef} />
    )
}
  1. 调用 React.useRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 指定 ref 为JSX属性,并向下传递
  3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ... 作为其第二个参数。
  4. 向下转发该 ref 参数到 ,将其指定为JSX属性
  5. 当 ref 挂载完成,inputRef.current 指向 input DOM节点

注意
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
在 React.forwardRef 之前,我们如果想传递 ref 属性给子组件,需要区分出是否是被HOC包装之后的组件,对使用来说,造成了一定的不便。我们来使用 React.forwardRef 重构。

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


function withData(WrappedComponent) {
    class ProxyComponent extends React.Component {
        componentDidMount() {
            //code
        }
        render() {
            const {forwardedRef, ...remainingProps} = this.props;
            return (
                <WrappedComponent ref={forwardedRef} {...remainingProps}/>
            )
        }
    }
    
    //我们在使用被withData包装过的组件时,只需要传 ref 即可
    const forwardRef = React.forwardRef((props, ref) => (
        <ProxyComponent {...props} forwardedRef={ref} />
    ));
    //指定 displayName.
    forwardRef.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
    return hoistNonReactStatic(forwardRef, WrappedComponent);
}
class MyInput extends React.Component {
    render() {
        return (
            <input type="text" {...this.props} />
        )
    }
}
MyInput.getName = function() {
    console.log('name');
}
MyInput = withData(MyInput);
console.log(MyInput.getName); //测试静态方法拷贝是否正常




function Form(props) {
    const inputRef = React.useRef(null);
    React.useEffect(() => {
        console.log(inputRef.current);//被包装组件MyInput
    })
    //在使用时,传递 ref 即可
    return (
        <MyInput ref={inputRef} />
    )
}

参考:
1、react中文官方文档:http://react.html.cn/docs/refs-and-the-dom.html
2、你想知道的关于 Refs 的知识都在这了
https://juejin.im/post/5db6506d6fb9a0207326a928

@LightXJ LightXJ changed the title react知识(一)— Refs和DOM React知识总结(一)— Refs和DOM Jul 11, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant