React 源码漂流(四)之 createRef

引言

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

1、Refs

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

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

在项目开发中,若是咱们可使用 声明式 或 提高 state 所在的组件层级(状态提高) 的方法来更新组件,最好不要使用 refs。git

使用场景

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

    在大多数状况下,咱们推荐使用受控组件来处理表单数据。在一个受控组件中,表单数据是由 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 实现

createRef 是 **React 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 并调用它。在 componentDidMount 或 componentDidUpdate 触发前,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 属性,由于它们没有实例。
  • 推荐使用 回调形式的 refs, stringRef 将会废弃(严格模式下使用会报警告),React.createRef() API 是 React v16.3 引入的更新。
  • 避免使用 refs 来作任何能够经过 声明式 实现来完成的事情

2、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 来实现。

3、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 渲染中介绍,敬请期待。

系列文章

想看更过系列文章,点击前往 github 博客主页

走在最后,欢迎关注:前端瓶子君,每日更新

前端瓶子君
相关文章
相关标签/搜索