Skip to content

React Ref ​

本节目标:理解“React Ref”的核心思路,并能把它用于实际开发或面试表达。

**ref** **的四种方式**
一、在 React v16.3 之前,ref 通过字符串(string ref)或者回调函数(callback ref)的形式进行获取。
ref 通过字符获取:
// string ref
class MyComponent extends React.Component {
    componentDidMount() {
        this.refs.myRef.focus();
    }
    render() {
        return <input ref="myRef" />;
    }
}
二、ref 通过回调函数获取:
// callback ref
class MyComponent extends React.Component {
    componentDidMount() {
        this.myRef.focus();
    }
    render() {
        return <input ref={(ele) => {
            this.myRef = ele;
        }} />;
    }
}
三、在 v16.3 中,提案引入了新的 API:React.createRef。ref 通过 React.createRef 获取:
// React.createRef
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    componentDidMount() {
        this.myRef.current.focus();
    }
    render() {
        return <input ref={this.myRef} />;
    }
}
四、hooks:useRef
function MyComponent() {
    const myRef = useRef(null);
    const onButtonClick = () => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
        myRef.current.focus();
    };
    return (
        <>
            <input ref={myRef} type="text" />
            <button onClick={onButtonClick}>聚焦</button>
        </>
    );
}
**将被移除的** **string ref**
string ref 不可组合。 例如一个第三方库的父组件已经给子组件传递了 ref,那么我们就无法在在子组件上添加 ref 了。 另一方面,回调引用没有一个所有者,因此您可以随时编写它们。例如:
/** string ref **/
class Parent extends React.Component {
    componentDidMount() {
        // 可获取到 this.refs.childRef
        console.log(this.refs);
    }
    render() {
        const { children } = this.props;
        return React.cloneElement(children, {
            ref: 'childRef',
        });
    }
}
class App extends React.Component {
    componentDidMount() {
        // this.refs.child 无法获取到
        console.log(this.refs);
    }
    render() {
        return (
            <Parent>
                <Child ref="child" />
            </Parent>
        );
    }
}
string ref 的所有者由当前执行的组件确定。 这意味着使用通用的“渲染回调”模式(例如react),错误的组件将拥有引用(它将最终在react上而不是您的组件定义renderRow)。
class MyComponent extends Component {
    renderRow = (index) => {
        // string ref 会挂载在 DataTable this 上
        return <input ref={'input-' + index} />;
        // callback ref 会挂载在 MyComponent this 上
        return <input ref={input => this['input-' + index] = input} />;
    }
    render() {
        return <DataTable
            data={this.props.data}
            renderRow={this.renderRow}
        />
    }
}
string ref 不适用于Flow之类的静态分析。 Flow不能猜测框架可以使字符串ref“出现”在react上的神奇效果,以及它的类型(可能有所不同)。 回调引用比静态分析更友好。

string ref 强制React跟踪当前正在执行的组件。 这是有问题的,因为它使react模块处于有状态,并在捆绑中复制react模块时导致奇怪的错误。在 reconciliation 阶段,React Element 创建和更新的过程中,ref 会被封装为一个闭包函数,等待 commit 阶段被执行,这会对 React 的性能产生一些影响。 关于这点可以参考 React 源码 coerceRef 的实现: 在调和子节点得过程中,会对 string ref 进行处理,把他转换成一个方法,这个方法主要做的事情就是设置 instance.refs[stringRef] = element,相当于把他转换成了function ref 对于更新得过程中string ref是否变化需要对比得是 current.ref._stringRef,这里记录了上一次渲染得时候如果使用得是string ref他的值是什么 owner是在调用createElement的时候获取的,通过ReactCurrentOwner.current获取,这个值在更新一个组件前会被设置,比如更新ClassComponent的时候,调用render方法之前会设置,然后调用render的时候就可以获取对应的owner了。 坚挺的 callback ref React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。 如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。 后来的 React.createRef React.createRef 的优点:

相对于 callback ref 而言 React.createRef 显得更加直观,避免了 callback ref 的一些理解问题。
React.createRef 的缺点:
性能略低于 callback ref

能力上仍逊色于 callback ref,例如上一节提到的组合问题,createRef 也是无能为力的。

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

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

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

  • 默认情况下,你不能在函数组件上使用 ref 属性(可以在函数组件内部使用),因为它们没有实例:

    如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle 结合使用)
    或者可以将该组件转化为 class 组件。
**useRef**
useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。**返回的** **ref** **对象在组件的整个生命周期内保持不变**。
并且 useRef 可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
正是由于这些特性,useRef 和 createRef 出现了很大差异。
可以运行下以下代码:
import React, { useState, useRef, useEffect } from "react";
export default function App() {
    const [count, setCount] = useState(0);
    const latestCount = useRef(count);
    useEffect(() => {
        latestCount.current = count;
    });
    function handleAlertclick() {
        setTimeout(() => {
            alert("latestCount.current:" + latestCount.current + '.. count: ' + count);
        }, 2000);
    }
    return (
        <div>
            <p>当前count: {count} </p>
            <button onClick={() => setCount(count + 1)}>count + 1</button>
            <button onClick={handleAlertclick}> 提示 </button>
        </div>
    )
}
然后按照下面步骤进行操作:
连续点击5次 count + 1 按钮
点击 提示 按钮
再点击完 提示 按钮后2秒内连续点击2次 count + 1 按钮
等待 alert 弹窗提示。

然后会你会得到一个有趣的答案:alert 弹窗会提示: latestCount.current:7.. count: 5。使用 useRef 能获取到最新的值,但是 useState 却不能。 具体原因可以参考 react 作者之一 dan 的个人博客。或者查看 React 函数式组件和类组件的区别,不是只有state和性能!

那么 useRef 真有那么很好用吗?并不是的。还有由于它上面的那个特性,问题还是不少的。
你可以尝试跑一下下面这段代码,或者 [点击这里查看](https://codesandbox.io/s/1rvwnj71x3)
import React, { useRef, createRef, useState } from "react";import ReactDOM from "react-dom";
function App() {  const [renderIndex, setRenderIndex] = useState(1);
const refFromUseRef = useRef();  const refFromCreateRef = createRef();
if (!refFromUseRef.current) {    // 赋值操作    refFromUseRef.current = renderIndex;  }  if (!refFromCreateRef.current) {    // 赋值操作    refFromCreateRef.current = renderIndex;  }  return (    <div className="App">      Current render index: {renderIndex}      <br />      在refFromUseRef.current中记住的第一个渲染索引:      {refFromUseRef.current}      <br />      在refFromCreateRef.current中未能成功记住第一个渲染索引:      {refFromCreateRef.current}      <br />      <button onClick={() => setRenderIndex(prev => prev + 1)}>        数值 + 1      </button>    </div>  );}
const rootElement = document.getElementById("root");ReactDOM.render(<App />, rootElement);复制代码
上面的案例中无论如何点击按钮 refFromUseRef.current 将始终为 1,而 renderIndex 和 refFromCreateRef.current 会伴随点击事件改变; 意想不到吧?
因为:当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 callback ref 来实现。
总结下:
useRef 可以获取 DOM ref
useRef 可以获取最新的值
useRef 内容发生改变并不会通知

由于上面的一些问题,起初我也是并不想把 useRef 作为操作 ref 的方法来讲的。

Refs 转发是否需要将 DOM Refs 暴露给父组件? 在极少数情况下,你可能希望在父组件中引用子节点的 DOM 节点。通常不建议这样做,因为它会打破组件的封装,但它偶尔可用于触发焦点或测量子 DOM 节点的大小或位置。

**如何将** **ref** **暴露给父组件?**
如果你使用 16.3 或更高版本的 React, 这种情况下我们推荐使用 ref 转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。
什么是 ref 转发?
const FancyButton = React.forwardRef((props, ref) => (
    <button ref={ref} className="FancyButton">
        {props.children}
    </button>
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
如果在低版本中如何转发?
如果你使用 16.2 或更低版本的 React,或者你需要比 ref 转发更高的灵活性,你可以使用 ref 作为特殊名字的 prop 直接传递。
比如下面这样:
function CustomTextInput(props) {  return (    <div>      <input ref={props.inputRef} />    </div>  );}
class Parent extends React.Component {  constructor(props) {    super(props);    this.inputElement = React.createRef();  }  render() {    return (      <CustomTextInput inputRef={this.inputElement} />    );  }}复制代码
以下是对上述示例发生情况的逐步解释:
我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>。
React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

> 来自

 <https://juejin.im/post/6883671829395800072>

本站总访问 — 次