深刻淺出React核心源碼解析(2) createElement與ReactElement

1、createElementjavascript

上一章咱們講到了全部jsx語法都會被轉成createElement。java

那麼createElement的實現是怎樣的呢?react

首先咱們從github克隆下來react的源碼庫,咱們先來分析下react源碼庫的文件佈局。git

react工程根目錄下有packages文件夾,其間放置的是react的各個包,咱們暫時把着力點放於react目錄下。內部是react源碼實現。github

拋出去一些非必要的檢測,和warn代碼,核心的react代碼其實只有幾百行。react源碼自己並不複雜,負責渲染的react-dom纔是最複雜的。數組

react目錄的src,就是react的核心實現了。數據結構

createElement方法位於ReactElement.js文件內,實現以下:dom

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是個工廠函數,返回一個對象。可是我我的以爲比較奇怪。

第1、工廠函數生成實例,這個工廠函數不應大寫開頭。

第2、使用構造函數或者類來聲明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;
}


複製代碼

2、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;
  }
}

複製代碼

3、總結

本章的重點在於,在react中,jsx標籤的本質就是ReactElement,createElement會對組件或者dom的type和props通過一層封裝處理,最後返回了ReactElement的實例。

相關文章
相關標籤/搜索