React16源码: createRef与forwardRef源码实现

ref 概述

  • 拿一个场景举例开始
    • 比如,在react当中写一个组件,类型是 class Component
    • 在 render function 里面渲染一系列的子组件或者 dom节点
    • 会有这样的需求,就是获取某个dom节点或某个子组件的实例来做一些手动的操作
    • 不局限于 props 更新这种方式来更新这个节点,获取节点后,可以绑定某些事件做一些操作
    • 如果没有其他好方法,可能要自己去写 document.querySelector之类的选择器来获取这个 dom 节点
  • 在 react 当中就为我们提供了这种方式
    • 通过 ref 这个特殊的 attribute 可以非常方便的在一个组件内部
    • 获取到它的一个子节点的实例对象, 这就是 ref 的一个核心功能
  • 在 react 当中有三种使用ref的方式
    • string ref
    • function
    • createRef

ref 应用示例

1 ) 演示代码

js 复制代码
import React from 'react'

export default class RefDemo extends React.Component {
  constructor() {
    super()
    this.objRef = React.createRef()
  }

  componentDidMount() {
    setTimeout(() => {
      this.refs.stringRef.textContent = 'string ref'
      this.methodRef.textContent = 'method ref'
      this.objRef.current.textContent = 'obj ref'
    }, 1000)
  }

  render() {
    return (
      <>
        <p ref="stringRef">xxxx</p>
        <p ref={ele => (this.methodRef = ele)}>yyyy</p>
        <p ref={this.objRef}>zzzz</p>
      </>
    )
  }
}

2 )代码说明

  • 第一种使用了 stringRef
    • 也就是我们在想要获取的那个节点的 props上面
    • 使用了一个 ref 属性, 然后传入一个字符串, react在完成这一个节点的渲染之后
    • 它会在 this.refs 这个对象上面挂载这个string对应的一个key
    • 这个key所指向的就是我们这个节点实例的对象
    • 如果是dom节点, 它就会对应dom的实例
    • 如果是子组件,就是子组件的实例,即:class Component
    • 如果是一个 function Component,正常来讲它是会失败的
    • 拿到的会是一个 undefined,因为 function Component 没有实例
    • 如果不让其出错,需要使用 forwardRef,后续来说
      * 此种方式不被推荐
  • 第二种使用了 function
    • 在 ref 属性上传入一个 method 方法,它接受一个参数
    • 这个参数就是一个节点对应的实例
      • 如果是dom节点对应的是dom实例
      • 如果是组件对应的是组件的实例
    • 然后在this上面去挂载某一个属性,比如上面的 methodRef, 基于其 textContent 属性来重新赋值
  • 第三种是通过 React.createRef 这个API
    • 在 class Component 内部,我们使用 this.objRef = React.createRef() 去创建了一个对象
      • 这个对象相当于 { current: null} , 默认值是 null
    • 把这个对象传给某一个节点的 ref 属性上,在组件渲染完成之后
    • 会把这个节点对应的实例挂载到这个对象的 current 这个属性上面
    • 调用它就是通过 this.objRef.current 操作它就可以了
  • 这个 demo 效果是不用跑也知道是怎样的一个变化
  • 以上三种情况的前两种没有在源码中有过多的体现,我们看下第三种情况 createRef

3 )源码探究

  • 从入口文件 React.js 中可见

    js 复制代码
    import {createRef} from './ReactCreateRef';
  • 定位到 ReactCreateRef.js 中

    js 复制代码
    /**
     * Copyright (c) Facebook, Inc. and its affiliates.
     *
     * This source code is licensed under the MIT license found in the
     * LICENSE file in the root directory of this source tree.
     * @flow
     */
    
    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;
    }
  • 这个代码看上去,非常简单,它返回的就是一个简单的对象

  • DEV 相关的判断不是核心,忽略即可

  • 它里面的属性 current, 默认值是 null

forward ref

1 )示例演示

  • 关于 forward ref, 先看一个示例

    js 复制代码
    import React from 'react'
    
    const TargetComponent = React.forwardRef((props, ref) => (
      <input type="text" ref={ref} />
    ))
    
    export default class Comp extends React.Component {
      constructor() {
        super()
        this.ref = React.createRef()
      }
    
      componentDidMount() {
        this.ref.current.value = 'ref input'
      }
    
      render() {
        return <TargetComponent ref={this.ref} />
      }
    }
    • 在这个文件下,定义了两个组件 Comp, TargetComponent
    • Comp 组件
      • 里面创建了一个ref, 并传递给 TargetComponent 组件
      • ref 是为了去获取某一个节点的实例的,通常会获取dom节点的实例
      • 有时,也会获取一下class Component 的实例
      • 如果组件是一个 纯的 function Component, 没有实例,则会出错
        • 如果我是一个组件的提供者,就是开源了一些组件
        • 用户(开发者) 一开始并不知道是否是有实例的组件
        • 还有就是,比如一些包, 如 Redux,提供的 connect 方法里的 HOC (高阶组件)
        • 作为用户想要获取自己写的组件的实例的时候,传入ref,获得的是被包装过的组件
      • 解决之道是外面套上 React.forwardRef,如上代码所示
        • forwardRef 可以帮助我们实现一个 ref 的传递
    • TargetComponent 组件
      • 就是我们基于 React.forwardRef 实现的 function Component
      • 如果要把它改造成HOC, 也可以基于此函数的回调将 ref 进行传递挂载
      • 这样就不会违背开发者的意图

2 )源码分析

定位到 forwardRef.js 中

js 复制代码
/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {REACT_FORWARD_REF_TYPE} from 'shared/ReactSymbols';

import warningWithoutStack from 'shared/warningWithoutStack';

export default function forwardRef<Props, ElementType: React$ElementType>(
  render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
  if (__DEV__) {
    if (typeof render !== 'function') {
      warningWithoutStack(
        false,
        'forwardRef requires a render function but was given %s.',
        render === null ? 'null' : typeof render,
      );
    } else {
      warningWithoutStack(
        // Do not warn for 0 arguments because it could be due to usage of the 'arguments' object
        render.length === 0 || render.length === 2,
        'forwardRef render functions accept exactly two parameters: props and ref. %s',
        render.length === 1
          ? 'Did you forget to use the ref parameter?'
          : 'Any additional parameter will be undefined.',
      );
    }

    if (render != null) {
      warningWithoutStack(
        render.defaultProps == null && render.propTypes == null,
        'forwardRef render functions do not support propTypes or defaultProps. ' +
          'Did you accidentally pass a React component?',
      );
    }
  }

  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
}
  • 忽略 DEV 判断代码,其本质上就只有一个 return 对象

    js 复制代码
    return {
      $$typeof: REACT_FORWARD_REF_TYPE,
      render,
    };
    • $$typeoff,是 REACT_FORWARD_REF_TYPE
      • 注意
        • 使用 forwordRef 时, 用这个API最终创建出来的 ReactElement
        • 比如上面示例的 TargetComponent组件最终会被创建成一个 ReactElement,比如,别名叫 A
        • TargetComponent组件 就是 forwordRef 返回的对象,这个不难理解,因为代码如此
        • TargetComponent 对应的 组件A 的 type 是 TargetComponent
        • TargetComponent 对应的 组件A的 $$typeofREACT_ELEMENT_TYPE
          • A的 $$typeof 不是 REACT_FORWARD_REF_TYPE
          • 因为,所有通过React.createElement创建的节点的 $$typeof 都是 REACT_ELEMENT_TYPE
          • TargetComponent组件最终会被React.createElement创建出来
          • 这一点不要搞混了
    • render
      • 就是我们传进来的 function component
相关推荐
栈老师不回家38 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙44 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨2 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos