React源码学习——从createElement开始

151 阅读4分钟

找到开始的着力点

正式的源码阅读,我先从react同名目录开始。进入index.js,看见所有的导出方法都是来自src/React.js。而在React.js中,导出的比较令我眼熟的方法主要有以下这些:

import {
  createElement as createElementProd,
} from './ReactElement';
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import {forwardRef} from './ReactForwardRef';
import {memo} from './ReactMemo';
import {block} from './ReactBlock';
import {
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useLayoutEffect,
  useMemo,
  useMutableSource,
  useReducer,
  useRef,
  useState,
  useTransition,
  useDeferredValue,
  useOpaqueIdentifier,
} from './ReactHooks';

所以很愉快的决定,从ReactElement.js开始看起,主要先来看createElement方法。

createElement

插曲

我先在createElement的方法下,加了一个debugger,然后运行代码。页面成功完成渲染,并没有运营到我打的debugger。这里其实就涉及到react16和react17的一个差异。

react16中,即使不使用react的api,只要用到jsx都需要这样引入import React from 'react'。因为需要通过@babel/preset-react将jsx的语法,转化为React.createElement的js代码。

而在react17中,直接通过react/jsx-runtime将jsx进行了转化,而不需要使用React.createElement了。因此我们在看react17时,直接在createElement中加入debugger,发现并没有走到。而在jsx/ReactJSXElement.js中的jsxDEV方法中加入debugger,会发现,走到了这个断点。

比较一下createElement和jsxDEV、jsx方法,会发现其代码基本是一致的,所以只是处理上的实现差异,基本的逻辑是不变的。所以我还是直接看createElement的代码。

是什么

在看createElement的代码实现前,首先是看它是干什么的。

简单的使用示例:

const el = React.createElement('div', {className: 'text'}, 'text')
const root = React.createElement('div', {className: 'root'}, el)
ReactDOM.render(root, document.getElementById('root'))

createElement的入参主要有三类:

type:需要创建的React元素的类型。可以是html的标签元素类型,也可以是react组件,或是react fragment类型

config:属性的对象

children:后续的参数都是当前元素的子节点。可以是字符串类型,及textContent节点。或者是React.createElement创建的元素(如示例中的两种情况)

createElement方法,返回ReactElement方法的结果。即返回一个React元素,供后续ReactDOM.render使用

怎么做

createElement的代码主要做了以下几件事:

  1. 将config中传入的ref、key、__self、__source进行单独存储
  2. 将除第一点中的四个属性外的其他属性,以及class组件的defaultProps中的属性都存储在props中
  3. 将从第三个参数开始的后续参数,看做子节点存在props.children中。如果只有一个节点则直接存,如果有多个节点,则以数组的形式存
  4. 返回ReactElement方法的结果,获得React Element元素
function createElement(type, config, children) {
  let propName;
  // 保存config参数中的属性
  const props = {};
  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  // 获取config中的属性,并保存至props参数中
  if (config != null) {
    // ref属性作为react中的特殊属性,单独记录在ref参数中
    if (hasValidRef(config)) {
      ref = config.ref;
      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    // key属性作为react中的特殊属性,单独记录在key参数中
    if (hasValidKey(config)) {
      key = '' + config.key; // 做了个转字符类型的处理
    }
    // 开发环境用于调试使用的属性(在ReactElement方法中的官方注释中有描述)
    // 需特殊处理,单独保存起来,并传给了ReactElement
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 除ref、可以、__self、__source外的其他config中的属性,添加至props中
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
  // 计算子节点的数量(前两个参数为type和config。减去这两个参数,剩余的都是子节点)
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    // 只有一个子节点参数时,props.children直接是当前子节点
    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;
  }
  // 对于class组件类型
  // 如果有defaultProps,则将其定义的属性加入props中
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    // 开发环境,对于通过props.key、props.ref调用的行为进行warning警告
    // 因为ref、key没有在props上,而是单独存起来了
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

ReactElement方法主要就是创建React元素。通过$$typeof可以识别是否是React元素,而不能通过instanceof来识别。

ReactElement将开发环境的那一段方法去掉,可以看到,它主要做的,就是加了一个React Element的标识

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 识别是React元素的标识
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录负责创建这个元素的组件
    _owner: owner,
  };

  if (__DEV__) {
    // 开发环境,将_store、_self、_source设为不可枚举的,方便React元素的比较
    element._store = {};
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
}

总结

今天先是找到我们开始看源码的第一个落脚点——createElement。然后再通过源码调试的方式,开始看起具体实现。其实官方也写了一些英文注释,我英文不好,使用翻译软件,也能大概了解到这些代码的主要用途。

对于createElement,它的主要作用是将babel转换完的jsx,解析ref、key、子节点等,形成React元素

接下来我打算看一下createElement之后的ReactDOM.render的实现