react 源码分析 之 createElement

85 阅读3分钟

1. createElement

这个函数 看名字也比较好理解,就是创建一个元素

一共接收三个 参数,

type:元素类型 (例如:span div)

config:配置属性 (例如 className)

children:子元素 (例如 <div><h1> hello world </h1></div>

image.png

export function createElement(type, config, children) {
  /**
   * propName -> 属性名称
   * 用于后面的 for 循环
   */
  let propName;
​
  /**
   * 存储 React Element 中的普通元素属性 即不包含 key ref self source
   */
  const props = {};
​
  /**
   * 待提取属性
   * React 内部为了实现某些功能而存在的属性
   */
  let key = null;
  let ref = null;
  let self = null;
  let source = null;
​
  // 如果 config 不为 null
  if (config != null) {
    // 如果 config 对象中有合法的 ref 属性
    if (hasValidRef(config)) {
      // 将 config.ref 属性提取到 ref 变量中
      ref = config.ref;
      // 在开发环境中
      if (__DEV__) {
        // 如果 ref 属性的值被设置成了字符串形式就报一个提示
        // 说明此用法在将来的版本中会被删除
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    // 如果在 config 对象中拥有合法的 key 属性
    if (hasValidKey(config)) {
      // 将 config.key 属性中的值提取到 key 变量中
      key = '' + config.key;
    }
​
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 遍历 config 对象
    for (propName in config) {
      // 如果当前遍历到的属性是对象自身属性
      // 并且在 RESERVED_PROPS 对象中不存在该属性
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        // 将满足条件的属性添加到 props 对象中 (普通属性)
        props[propName] = config[propName];
      }
    }
  }
​
  /**
   * 将第三个及之后的参数挂载到 props.children 属性中
   * 如果子元素是多个 props.children 是数组
   * 如果子元素是一个 props.children 是对象
   */
​
  // 由于从第三个参数开始及以后都表示子元素
  // 所以减去前两个参数的结果就是子元素的数量
  const childrenLength = arguments.length - 2;
  // 如果子元素的数量是 1
  if (childrenLength === 1) {
    // 直接将子元素挂载到到 props.children 属性上
    // 此时 children 是对象类型
    props.children = children;
    // 如果子元素的数量大于 1
  } else if (childrenLength > 1) {
    // 创建数组, 数组中元素的数量等于子元素的数量
    const childArray = Array(childrenLength);
    // 开启循环 循环次匹配子元素的数量
    for (let i = 0; i < childrenLength; i++) {
      // 将子元素添加到 childArray 数组中
      // i + 2 的原因是实参集合的前两个参数不是子元素
      childArray[i] = arguments[i + 2];
    }
    // 如果是开发环境
    if (__DEV__) {
      // 如果 Object 对象中存在 freeze 方法
      if (Object.freeze) {
        // 调用 freeze 方法 冻结 childArray 数组
        // 防止 React 核心对象被修改 冻结对象提高性能
        Object.freeze(childArray);
      }
    }
    // 将子元素数组挂载到 props.children 属性中
    props.children = childArray;
  }
​
  /**
   * 如果当前处理是组件
   * 看组件身上是否有 defaultProps 属性
   * 这个属性中存储的是 props 对象中属性的默认值
   * 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined
   * 如果为undefined 就将默认值赋值给对应的 props 属性值
   */
​
  // 将 type 属性值视为函数 查看其中是否具有 defaultProps 属性
  if (type && type.defaultProps) {
    // 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量
    const defaultProps = type.defaultProps;
    // 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量
    for (propName in defaultProps) {
      // 如果 props 对象中的该属性的值为 undefined
      if (props[propName] === undefined) {
        // 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值
        props[propName] = defaultProps[propName];
      }
    }
  }
​
  /**
   * 在开发环境中 React 会检测开发者是否在组件内部
   * 通过 props 对象获取 key 属性或者 ref 属性
   * 如果开发者调用了 在控制台中报错误提示
   */
​
  // 如果处于开发环境
  if (__DEV__) {
    // 元素具有 key 属性或者 ref 属性
    if (key || ref) {
      // 看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件
      // 如果元素不是组件 就直接返回元素类型字符串
      // displayName 用于在报错过程中显示是哪一个组件报错了
      // 如果开发者显式定义了 displayName 属性 就显示开发者定义的
      // 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      // 如果 key 属性存在
      if (key) {
        // 为 props 对象添加key 属性
        // 并指定当通过 props 对象获取 key 属性时报错
        defineKeyPropWarningGetter(props, displayName);
      }
      // 如果 ref 属性存在
      if (ref) {
        // 为 props 对象添加 ref 属性
        // 并指定当通过 props 对象获取 ref 属性时报错
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  // 返回 ReactElement
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    // 在 Virtual DOM 中用于识别自定义组件
    ReactCurrentOwner.current,
    props,
  );
}
其实从上面的源码 以及 我们的备注可以看出,其实这个函数的主要作用就是几个
  1. 分离 react 特有的 props 属性 和 元素的一般属性

    1. 特殊属性有 key ref _source _self
  2. 将当前元素的 子节点 挂载到 props 上

  3. 如果当前type 是组件 则需要 将type 的默认属性 defaultProps 赋值给props

    1. image.png
  4. 最后就是 调用 ReactElement 方法 创建元素 并将其 返回

    1. image.png

2. ReactElement

既然上面说了 createElement 最后返回的 是 ReactElement 处理过后的元素,那么我们现在就来看一下 ReactElemnt 这个函数内部的实现

先来看看这个函数的入参

image.png

  • type

    • 这个参数就是我们的元素节点 例如 span div h1 这种元素
    • 或者是 我们的函数组件
  • key

    • 元素的唯一标识
    • 用于内部提升 virtual dom diff 性能的
  • ref

    • 存储的就是 dom 对象,或者是 组件的实例对象
    • 我们平常会使用 ref 来获取对象,并对这个对象进行操作,例如我们的 form 表单验证,或者是回显操作,就经常会使用到
  • self

    • 这个是不会直接放到我们的 返回的 element 对象身上
    • self 和 source 都是在 dev 环境下才会有的属性
  • source

    • 跟 self 一样,只会在 dev 环境下存在
    • 用于测试目的,在不同位置创建的元素 elements 应该是相等的
  • owner

    • 记录当前 elements 是由谁创建来的
  • props

    • 存储着用来组件内部传递的数据 例如:className children

还有一个就是 $$typeof: REACT_ELEMENT_TYPE,

这个属性是组件的类型,16进制存错的,react 最终在渲染 dom 的时候会使用到

这个函数做的事也比较简单,就是将传进来的参数,进行组装成一个 elements 对象,然后进行返回。

到这里就算是把这 createElement 这个函数给分析完了