深入浅出React核心源码解析(2) createElement与ReactElement

一、createElement

上一章我们讲到了所有jsx语法都会被转成createElement。

那么createElement的实现是怎样的呢?

首先我们从github克隆下来react的源码库,我们先来分析下react源码库的文件布局。

react工程根目录下有packages文件夹,其间放置的是react的各个包,我们暂时把着力点放于react目录下。内部是react源码实现。

抛出去一些非必要的检测,和warn代码,核心的react代码其实只有几百行。react源码本身并不复杂,负责渲染的react-dom才是最复杂的。

react目录的src,就是react的核心实现了。

createElement方法位于ReactElement.js文件内,实现如下:


export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    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;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    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,
  );
}


复制代码

这里面有一些开发环境下检测,和外部调用方法,可能会使阅读者精力分散,我们来稍微改动精简下代码,使功能一致,同时更好阅读:

export function createElement(type, config, ...children) {
  const {ref = null, key = null} = config || {};
  const {current} = ReactCurrentOwner;
  const {defaultProps} = type || {};
  const props = assignProps(config, defaultProps, children);

  return new ReactElement({
    type,
    key: '' + key,
    ref,
    current,
    props,
  });
}
复制代码

经过精简和简化后,createElement仅有30行代码。我们来逐行解析下。

/**
 * 
 * @param type {string | function | object}  
 *        如果type是字符串,那就是原生dom元素,比如div
 *        如果是function或者是Component的子类 则是React组件
 *        object 会是一些特殊的type 比如fragment
 * @param config {object}
 *        props 和key 还有ref 其实都是在config里了
 * @param children
 *        就是由其他嵌套createElement方法返回的ReactElement实例
 * @returns {ReactElement}
 * 
 */
export function createElement(type, config, ...children) {
    
  // 给config设置一个空对象的默认值
  // ref和key 默认为null
  const {ref = null, key = null} = config || {};
  // ReactCurrentOwner负责管理当前渲染的组件和节点
  const {current} = ReactCurrentOwner;
  // 如果是函数组件和类组件 是可以有defaultProps的
  // 比如
  // function A({age}) {return <div>{age}</div>}
  // A.defaultProps = { age:123 }
  const {defaultProps} = type || {};
  // 把defaultProps和props 合并一下
  const props = assignProps(config, defaultProps, children);
  // 返回了一个ReactElement实例
  return new ReactElement({
    type,
    key: '' + key,
    ref,
    current,
    props,
  });
}

复制代码

ref和key不用多说,大家都知道是干啥的。之前有个同事问过我,key明明传的是数字,为啥最后成了字符串,症结就在上面的ReactELement构造函数传参的key那里了,key:''+key

assignProps是我抽象了一个方法,合并defaultProps和传入props的方法,稍后提供代码,其实在cloneElement方法里,也有一段类似代码,但是react并没有抽象出来,相对来说,会有代码冗余,暂且提炼出来。

重点在new ReactElement()。

react的代码里,ReactElement是个工厂函数,返回一个对象。但是我个人觉得比较奇怪。

第一、工厂函数生成实例,这个工厂函数不该大写开头。

第二、使用构造函数或者类来声明ReactElement难道不是一个更好,更符合语义的选择?

在这里,为了便于理解,把ReactElement从工厂函数,改变成了一个类,createElement返回的就是一个ReactElement类的实例。

下面看下asssignProps的实现,该方法在cloneElement也可以复用:


const RESERVED_PROPS = ['key', 'ref', '__self', '__source'];

export function assignProps(config, defaultProps, children) {
  
    const props = {
        children,
    };
    config = config || {};
    for (const propName in config) {
        if (
            config.hasOwnProperty(propName) &&
            !RESERVED_PROPS.includes(propName)
        ) {
            props[propName] = config[propName];
            if (
                props[propName] === undefined &&
                defaultProps &&
                defaultProps[propName] !== undefined
            ) {
                props[propName] = defaultProps[propName];
            }
        }
    }

    return props;
}


复制代码

二、ReactElement

create返回的是个ReactElement实例,那么ReactElement又是啥呢?

抛出去dev时的代码,精简后如下:


const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};


复制代码

可以看到,其实就是返回了一个对象,我们现在可以简单而浮夸的想象下,react的render机制其实就是读取这些数据结构,然后根据结构树,层层根据原生dom方法渲染而成。(暂时这样想象)

经过用类改造后的代码为:


export class ReactElement {
  constructor(elementParams) {
    const {type, key, ref, current, props} = elementParams || {};
    // 如果是原生标签比如h1 那就是字符串
    // 如果是组件 则是组件的引用
    this.type = type;
    // key
    this.key = key;
    // ref
    this.ref = ref;
    // 延后再讲
    this._owner = current;
    // props
    this.props = props;
    // 类型标识 新版本中的React里是symbo
    this.$$typeof = REACT_ELEMENT_TYPE;
  }
}

复制代码

三、总结

本章的重点在于,在react中,jsx标签的本质就是ReactElement,createElement会对组件或者dom的type和props经过一层封装处理,最后返回了ReactElement的实例。

猜你喜欢

转载自blog.csdn.net/weixin_33966095/article/details/91375208