【React源码 - ReactDom.render发生了什么】

在React开发中,在入口文件我们都会执行ReactDom.render来讲整个应用挂载在主document中,那其中发生了什么,React是如何讲我们写的JSX代码,一步一步更新Fiber进而挂载渲染的呢。本文主要是基于[email protected]的源码以及自己的理解来简单介绍一下这里面发生了什么。

流程总览

如下图所示,总体流程就是开发编写JSX代码之后会交由Babel来进行转移,并调用React.createElement转换为reactElement,然后生成虚拟DOM,通过render函数进一步更新filber渲染在页面上。所以本文主要是从源码角度来分析createElement和ReactElement函数的内部实现逻辑。
【React源码 - ReactDom.render发生了什么】_第1张图片

createElement

函数文件路径:packages/react/src/ReactElement.js

该函数接收三个参数(type、config、children),然后创建并返回一个给定类型的新ReactElement。主要做了一下一些事:

  1. 从config参数中拆分props属性和特殊属性
  2. 将子元素挂载到props.children上
  3. 给未设置的props属性设置默认值defaultProps
  4. 创建并返回一个ReactElement

由于开发环境下的代码主要是一些log或者warn的提示信息,所以这里不再分析,下面请看源码解析:
第一步:从config参数中拆分props属性和特殊属性

/**
   * 定义props的属性名,用于后面两次for遍历
   * 一次是遍历config配置,并将props属性拆出来
   * 一次是遍历defaultProps默认属性,给未赋值的属性给默认值
   */
  let propName;

  // 保留props属性配置,不包括key、ref、self、source
  const props = {};

  // config中的react内置的特殊属性
  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // 处理config参数,主要就是拆分props和特殊属性
  if (config != null) {
    // 将ref赋予有效值,用来保存当前的dom元素
    if (hasValidRef(config)) {
      ref = config.ref;
      // 开发环境提示warn信息
      if (__DEV__) {
        // ref属性设置成字符串的一个提示,后续版本会删除该用法
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    // 以字符串的形式保留key属性
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    /**
     * 如果没有self、source属性则设置为null
     * 个人猜测可能是由于undefined可以被重写,所以这里改为null
     * 而且null比undefined更符合在这里的语义
     *  */
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 去除key、ref、self、source特殊属性并赋值给props保存
    // RESERVED_PROPS定义的包含key、ref、self、source的对象
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

第二步:将子元素挂载到props.children上

/**
   * arguments是参数合集数组,包含上面的三个行参type、config、children...
   * 这里arguments.length - 2就是获取后面所有children的长度
   */
  const childrenLength = arguments.length - 2;
  // 如果只有一个子元素,则直接赋值
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    // 如果有多个,则通过数组来保存,并存储在props.children中
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

第三步:给未设置的props属性设置默认值defaultProps

// 遍历type元素的默认类型,给未设置的属性赋予默认值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

第四步:创建并返回一个ReactElement
通过将上面解析点属性配置,传递给ReactElement函数,然后返回一个ReactElement类型的DOM元素

return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );

ReactElement

函数文件路径:packages/react/src/ReactElement.js
该函数使用工厂模式创建一个新的React Element,不在使用类的模式,所以不能通过new的方式来获取实例,该函数接收7个参数,并返回一个react element元素,不能用instanceof来判断该Element是不是React Element,只能通过Symbol.for(‘react.element’)来判断是否是React Element。react中是通过isValidElement方法判断元素是否为空对象并且$$typeof 是否为 REACT_ELEMENT_TYPE来判断的

/**
 * Verifies the object is a ReactElement.
 * See https://reactjs.org/docs/react-api.html#isvalidelement
 * @param {?object} object
 * @return {boolean} True if `object` is a ReactElement.
 * @final
 */
export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

/**
 * Factory method to create a new React element. This no longer adheres to
 * the class pattern, so do not use new to call it. Also, instanceof check
 * will not work. Instead test $$typeof field against Symbol.for('react.element') to check
 * if something is a React Element.
 *
 * @param {*} type 元素类型(dov、span...)
 * @param {*} props 子元素和元素基础属性
 * @param {*} key 元素的唯一标识,用于diff算法,节点对比
 * @param {string|object} ref 元素实例
 * @param {*} owner 父组件或者上级组件
 * @param {*} self 用于辅助判断this指向,后续会结合箭头函数去掉owner和字符串类型的ref参数
 * @param {*} source 保存文件名、行号和/或其他信息的资源数据
 * @internal
 */
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    /**
     * 通过这个属性唯一标识是否是react element类型的元素
     * React在最后渲染的时候,会确认元素的类型是REACT_ELEMENT_TYPE
     * 表示组件元素的类型,值为十六进制或者Symbol值
     */
    $$typeof: REACT_ELEMENT_TYPE,

    // 元素类型 (div、span...)
    type: type,
    // 元素唯一标识 diff中vmdom和真实dom对比时,首先判断key
    key: key,
    // 组件实例
    ref: ref,
    // 向组件内部传递的属性数据
    props: props,

    // 父组件或者上级组件
    _owner: owner,
  };

  // 返回一个带有react element表示的dom元素
  return element;
};

ReactDOM.render

函数文件路径:packages/react-dom/src/client/ReactDOMLegacy.js
创建一个React Element元素之后,会传入render函数中进行渲染,render函数接收三个参数element、container、callback(可选), 然后调用legacyRenderSubtreeIntoContainer函数在container容器中调用react-reconciler中的updateContainer来更新队列以及整个Fiber。

/**
 * 
 * @param {*} element 元素类型
 * @param {*} container element挂载在那些容器组件下
 * @param {*} callback 可选,渲染完成之后执行的回调
 * @returns 
 */

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  // 判断传入的父容器是否有效
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );

  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

从render到更新fiber进而更新真实dom渲染的大致流程如下:
【React源码 - ReactDom.render发生了什么】_第2张图片

你可能感兴趣的:(react.js,javascript,前端)