女王控的博客

React之createRef入门学习

引言

本篇从 React Refs 的使用场景、使用方式、注意事项,到 createRef 与 Hook useRef 的对比使用,最后以 React createRef 源码结束,剖析整个 React Refs,关于 React.forwardRef 会在下一篇文章深入探讨。

Refs

React 的核心思想是每次对于界面 state 的改动,都会重新渲染整个Virtual DOM,然后新老的两个 Virtual DOM 树进行 diff(协调算法),对比出变化的地方,然后通过 render 渲染到实际的UI界面,

使用 Refs 为我们提供了一种绕过状态更新和重新渲染时访问元素的方法;这在某些用例中很有用,但不应该作为 propsstate 的替代方法。

在项目开发中,如果我们可以使用 声明式 或 提升 state 所在的组件层级(状态提升) 的方法来更新组件,最好不要使用 refs。

使用场景

  • 管理焦点(如文本选择)或处理表单数据: Refs 将管理文本框当前焦点选中,或文本框其它属性。

    在大多数情况下,我们推荐使用受控组件来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的,每个状态更新都编写数据处理函数。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。要编写一个非受控组件,就需要使用 Refs 来从 DOM 节点中获取表单数据。

    class NameForm extends React.Component {
    constructor(props) {
      super(props);
      this.input = React.createRef();
    }
    
    handleSubmit = (e) => {
      console.log('A name was submitted: ' + this.input.current.value);
      e.preventDefault();
    }
    
    render() {
      return (
        <form onSubmit={this.handleSubmit}>
          <label>
            Name:
            <input type="text" ref={this.input} />
          </label>
          <input type="submit" value="Submit" />
        </form>
      );
    }
    }

    因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

  • 媒体播放:基于 React 的音乐或视频播放器可以利用 Refs 来管理其当前状态(播放/暂停),或管理播放进度等。这些更新不需要进行状态管理。

  • 触发强制动画:如果要在元素上触发过强制动画时,可以使用 Refs 来执行此操作。

  • 集成第三方 DOM 库

使用方式

Refs 有 三种实现:

方法一:通过 createRef 实现

createRefReact v16.3 新增的API,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。

Refs 通常在 React 组件的构造函数中定义,或者作为函数组件顶层的变量定义,然后附加到 render() 函数中的元素。

export default class Hello extends React.Component {
  constructor(props) {
    super(props);
    // 创建 ref 存储 textRef DOM 元素
    this.textRef = React.createRef(); 
  }
  componentDidMount() {
    // 注意:通过 "current" 取得 DOM 节点
    // 直接使用原生 API 使 text 输入框获得焦点
    this.textRef.current.focus(); 
  }
  render() {
    // 把 <input> ref 关联到构造器里创建的 textRef 上
    return <input ref={this.textRef} />
  }
}	

使用 React.createRef() 给组件创建了 Refs 对象。在上面的示例中,ref被命名 textRef,然后将其附加到 <input> DOM元素。

其中, textRef 的属性 current 指的是当前附加到 ref 的元素,并广泛用于访问和修改我们的附加元素。事实上,如果我们通过登录 myRef 控制台进一步扩展我们的示例,我们将看到该 current 属性确实是唯一可用的属性:

componentDidMount = () => {
   // myRef 仅仅有一个 current 属性
   console.log(this.textRef);
   // myRef.current
   console.log(this.textRef.current);
   // component 渲染完成后,使 text 输入框获得焦点
   this.textRef.current.focus();
}

componentDidMount 生命周期阶段,myRef.current 将按预期分配给我们的 <input> 元素; componentDidMount 通常是使用 refs 处理一些初始设置的安全位置。

我们不能在 componentWillMount 中更新 Refs,因为此时,组件还没渲染完成, Refs 还为 null

方法二:回调 Refs

不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

import React from 'react';
export default class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.textRef = null; // 创建 ref 为 null
  }
  componentDidMount() {
    // 注意:这里没有使用 "current" 
    // 直接使用原生 API 使 text 输入框获得焦点
    this.textRef.focus(); 
  }
  render() {
    // 把 <input> ref 关联到构造器里创建的 textRef 上
    return <input ref={node => this.textRef = node} />
  }
}

React 将在组件挂载时将 DOM 元素传入ref 回调函数并调用,当卸载时传入 null 并调用它。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。

像上例, ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。

这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。我们可以通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

方法三:通过 stringRef 实现

export default class Hello extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    // 通过 this.refs 调用
    // 直接使用原生 API 使 text 输入框获得焦点
    this.refs.textRef.focus(); 
  }
  render() {
    // 把 <input> ref 关联到构造器里创建的 textRef 上
    return <input ref='textRef' />
  }
}

尽管字符串 stringRef 使用更方便,但是它有一些缺点,因此严格模式使用 stringRef 会报警告。官方推荐采用回调 Refs。

注意

  • ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref ,我们可以通过 Refs 访问 DOM 元素属性。
  • ref 属性被用于一个自定义 class 组件时,ref 对象将接收该组件已挂载的实例作为它的 current,与 ref 用于 HTML 元素不同的是,我们能够通过 ref 访问该组件的props,state,方法以及它的整个原型 。
  • ref 是为了获取某个节点是实例,所以 你不能在函数式组件上使用 ref 属性,因为它们没有实例。
  • 推荐使用 回调形式的 refsstringRef 将会废弃(严格模式下使用会报警告),React.createRef() API 是 React v16.3 引入的更新。
  • 避免使用 refs 来做任何可以通过 声明式 实现来完成的事情

createRef 与 Hook useRef

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的值。返回的 ref 对象在组件的整个生命周期内保持不变。

function Hello() {
  const textRef = useRef(null)
  const onButtonClick = () => {
    // 注意:通过 "current" 取得 DOM 节点
    textRef.current.focus();
  };
  return (
    <>
      <input ref={textRef} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}

区别

useRef()ref 属性更有用。useRef() Hook 不仅可以用于 DOM refs, useRef() 创建的 ref 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

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

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...}对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

createRef 源码解析

// ReactCreateRef.js 文件
import type {RefObject} from 'shared/ReactTypes';

// an immutable object with a single mutable value
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    // 封闭对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
    Object.seal(refObject); 
  }
  return refObject;
}

其中 RefObject 为:

export type RefObject = {
  current: any,
};

这就是的 createRef 源码,实现很简单,但具体的它如何使用,如何挂载,将在后面的 React 渲染中介绍,敬请期待。

评论

阅读下一篇

React之forwardRef入门学习
2019-09-20 14:35:44
0%