React系列 --- createElement, ReactElement與Component部分源碼解析(五)

React系列

React系列 --- 簡單模擬語法(一)
React系列 --- Jsx, 合成事件與Refs(二)
React系列 --- virtualdom diff算法實現分析(三)
React系列 --- 從Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement與Component部分源碼解析(五)
React系列 --- 從使用React瞭解Css的各類使用方案(六)
React系列 --- 從零構建狀態管理及Redux源碼解析(七)
React系列 --- 擴展狀態管理功能及Redux源碼解析(八)html

前言

由於以前寫過一些文章分別關於怎麼模擬React語法,React基本知識和virtualdom diff實現思路,接下來就跟着React源碼大概瞭解一下怎麼一個過程,只是主邏輯代碼,忽略部分開發環境的代碼.
如下解析僅限於我當時理解,不必定準確.react

JSX編譯

仍是用以前的例子git

<div className="num" index={1}>
  <span>123456</span>
</div>

編譯成github

React.createElement("div", {
  className: "num",
  index: 1
}, React.createElement("span", null, "123456"));

createElement

咱們看下API的語法算法

React.createElement(
  type,
  [props],
  [...children]
)

建立並返回給定類型的新 React element 。segmentfault

參數 描述
type 既能夠是一個標籤名稱字符串,也能夠是一個 React component 類型(一個類或一個函數),或者一個React fragment 類型
props 各類屬性值
children 子元素

由於有babel會編譯JSX,因此通常不多會直接調用這個方法.api

而後咱們進入找到對應的源碼位置查看代碼 react/packages/react/src/ReactElement.jsbabel

/**
 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
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;

  // 有傳config的狀況下
  if (config != null) {
    // 是否有有效的Ref
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // 是否有有效的Key
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // 暫時還沒聯繫上下文,保存self和source
    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) {
      // 若是type是函數說明不是原生dom,因此能夠取一下幾個值 
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;

      // 定義key屬性的取值器,添加對應警告
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      // 定義ref屬性的取值器,添加對應警告
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

代碼還比較簡單,能夠看出就是傳入參數以後它會幫你作些特殊處理而後導出給ReactElement方法使用,若是有部分代碼還不知道是幹嗎的話也不用擔憂,下面會有說到app

function hasValidRef(config) {
  // 開發環境下
  if (__DEV__) {
    // 自身是否含有ref字段
    if (hasOwnProperty.call(config, 'ref')) {
      // 獲取它的取值器
      const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
      // 知足條件的話爲非法ref
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  // 直接和undefined做比較判斷是否合法
  return config.ref !== undefined;
}

同上
function hasValidKey(config) {
  // 開發環境
  if (__DEV__) {
    if (hasOwnProperty.call(config, 'key')) {
      const getter = Object.getOwnPropertyDescriptor(config, 'key').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  return config.key !== undefined;
}
// 初始化標記
let specialPropKeyWarningShown, specialPropRefWarningShown;

// 定義key的取值器
function defineKeyPropWarningGetter(props, displayName) {
  const warnAboutAccessingKey = function() {
    if (!specialPropKeyWarningShown) {
      specialPropKeyWarningShown = true;
      // 目測是警告提示
      warningWithoutStack(
        false,
        '%s: `key` is not a prop. Trying to access it will result ' +
          'in `undefined` being returned. If you need to access the same ' +
          'value within the child component, you should pass it as a different ' +
          'prop. (https://fb.me/react-special-props)',
        displayName,
      );
    }
  };
  // 是否已經警告過
  warnAboutAccessingKey.isReactWarning = true;
  // 定義key字段
  Object.defineProperty(props, 'key', {
    get: warnAboutAccessingKey,
    configurable: true,
  });
}

// 同上
function defineRefPropWarningGetter(props, displayName) {
  const warnAboutAccessingRef = function() {
    if (!specialPropRefWarningShown) {
      specialPropRefWarningShown = true;
      warningWithoutStack(
        false,
        '%s: `ref` is not a prop. Trying to access it will result ' +
          'in `undefined` being returned. If you need to access the same ' +
          'value within the child component, you should pass it as a different ' +
          'prop. (https://fb.me/react-special-props)',
        displayName,
      );
    }
  };
  warnAboutAccessingRef.isReactWarning = true;
  Object.defineProperty(props, 'ref', {
    get: warnAboutAccessingRef,
    configurable: true,
  });
}

代碼來看是開發模式下限制了對應keyref的取值器,使用時會執行對應方法進行報錯不讓讀取.dom

至此相關源碼基本瞭解了

getOwnPropertyDescriptor

上面其中核心方法介紹是這個

Object.getOwnPropertyDescriptor(obj, prop)

方法返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,不須要從原型鏈上進行查找的屬性)

參數 描述
obj 須要查找的目標對象
prop 目標對象內屬性名稱
返回值 若是指定的屬性存在於對象上,則返回其屬性描述符對象(property descriptor),不然返回 undefined

屬性描述符對象(property descriptor)

該方法容許對一個屬性的描述進行檢索。在 Javascript 中, 屬性 由一個字符串類型的「名字」(name)和一個「屬性描述符」(property descriptor)對象構成。

一個屬性描述符是一個記錄,由下面屬性當中的某些組成的:

屬性 描述
value 該屬性的值(僅針對數據屬性描述符有效)
writable 當且僅當屬性的值能夠被改變時爲true。(僅針對數據屬性描述有效)
get 獲取該屬性的訪問器函數(getter)。若是沒有訪問器, 該值爲undefined。(僅針對包含訪問器或設置器的屬性描述有效)
set 獲取該屬性的設置器函數(setter)。 若是沒有設置器, 該值爲undefined。(僅針對包含訪問器或設置器的屬性描述有效)
configurable 當且僅當指定對象的屬性描述能夠被改變或者屬性可被刪除時,爲true。
enumerable 當且僅當指定對象的屬性能夠被枚舉出時,爲 true。

ReactElement

而後再看看ReactElement的源碼

/**
 * 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, no instanceof check
 * will work. Instead test $$typeof field against Symbol.for('react.element') to check
 * if something is a React Element.
 *
 * @param {*} type
 * @param {*} props
 * @param {*} key
 * @param {string|object} ref
 * @param {*} owner
 * @param {*} self A *temporary* helper to detect places where `this` is
 * different from the `owner` when React.createElement is called, so that we
 * can warn. We want to get rid of owner and replace string `ref`s with arrow
 * functions, and as long as `this` and owner are the same, there will be no
 * change in behavior.
 * @param {*} source An annotation object (added by a transpiler or otherwise)
 * indicating filename, line number, and/or other information.
 * @internal
 */
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  // 開發模式下增改部分屬性
  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

整段代碼來看它是在開發環境下對字段做處理:

  • 建立React元素,設置對應屬性值
  • 開發環境下

    • 建立_store屬性並配置其validated的屬性描述符對象,達到方便調試React元素的目的
    • 配置_self的屬性描述符對象,selfsource只是DEV的屬性
    • 配置_source的屬性描述符對象,出於測試的目的,應該將在兩個不一樣位置建立的兩個元素視爲相等的,所以咱們將它從枚舉中隱藏起來。
    • 凍結element及其props對象

主要目的是爲提升測試環境下效率,將element的一些屬性配置爲不可枚舉,進行遍歷的時候跳過這些屬性。

其中REACT_ELEMENT_TYPE

// The Symbol used to tag the ReactElement type. If there is no native Symbol
// nor polyfill, then a plain number is used for performance.
var REACT_ELEMENT_TYPE =
  (typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
  0xeac7;

用來標識這個對象是一個ReactElement對象,至此Jsx編譯成ReactElement對象的相關源碼大概知道了.

React組件建立

createClass

16.0.0之後已經廢棄,可忽略

ES6類繼承

從官方例子來看React.Component

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

從源碼看這個類寫法作了什麼 react/packages/react/src/ReactBaseClasses.js

const emptyObject = {};
if (__DEV__) {
  Object.freeze(emptyObject);
}

/**
 * Base class helpers for the updating state of a component.
 */
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

結構比較簡單,結合註釋能夠知道只是基本賦值,裏面有個更新器後面再說,如今先記住是用來更新State就好了.

/**
 * Sets a subset of the state. Always use this to mutate
 * state. You should treat `this.state` as immutable.
 *
 * There is no guarantee that `this.state` will be immediately updated, so
 * accessing `this.state` after calling this method may return the old value.
 *
 * There is no guarantee that calls to `setState` will run synchronously,
 * as they may eventually be batched together.  You can provide an optional
 * callback that will be executed when the call to setState is actually
 * completed.
 *
 * When a function is provided to setState, it will be called at some point in
 * the future (not synchronously). It will be called with the up to date
 * component arguments (state, props, context). These values can be different
 * from this.* because your function may be called after receiveProps but before
 * shouldComponentUpdate, and this new state, props, and context will not yet be
 * assigned to this.
 *
 * @param {object|function} partialState Next partial state or function to
 *        produce next partial state to be merged with current state.
 * @param {?function} callback Called after state is updated.
 * @final
 * @protected
 */
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

註釋很長實際代碼很短,大概意思就是

  • 不保證this.state會當即更新,因此調用方法以後可能獲取的舊數據
  • 不保證this.state會同步運行,可能最終他們會批量組合執行,能夠提供一個可選完成回調當更新以後再執行
  • 回調會在將來某個點執行,能夠拿到最新的入參(state, props, context),不一樣於this.XX,由於它會在shouldComponentUpdate以前接收新的props屬性以後執行,此時還沒賦值給this.
/**
 * Forces an update. This should only be invoked when it is known with
 * certainty that we are **not** in a DOM transaction.
 *
 * You may want to call this when you know that some deeper aspect of the
 * component's state has changed but `setState` was not called.
 *
 * This will not invoke `shouldComponentUpdate`, but it will invoke
 * `componentWillUpdate` and `componentDidUpdate`.
 *
 * @param {?function} callback Called after update is complete.
 * @final
 * @protected
 */
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

這是強制更新視圖的方法

  • 這應該只在肯定咱們不是在一個DOM事務中時調用
  • 這應該在當你知道某些深層嵌套組件狀態已變可是沒有執行setState的時候調用
  • 它不會執行shouldComponentUpdate但會執行componentWillUpdatecomponentDidUpdate生命週期

接着咱們看源碼 react/packages/shared/invariant.js作了什麼

/**
 * Use invariant() to assert state which your program assumes to be true.
 *
 * Provide sprintf-style format (only %s is supported) and arguments
 * to provide information about what broke and what you were
 * expecting.
 *
 * The invariant message will be stripped in production, but the invariant
 * will remain to ensure logic does not differ in production.
 */

export default function invariant(condition, format, a, b, c, d, e, f) {
  throw new Error(
    'Internal React error: invariant() is meant to be replaced at compile ' +
      'time. There is no runtime version.',
  );
}

使用constant()斷言程序假定爲真的狀態,僅用於開發,會從生產環境中剝離保證不受影響

接下來咱們再看看 react/packages/react/src/ReactNoopUpdateQueue.js

/**
 * This is the abstract API for an update queue.
 */
const ReactNoopUpdateQueue = {
  /**
   * Checks whether or not this composite component is mounted.
   * @param {ReactClass} publicInstance The instance we want to test.
   * @return {boolean} True if mounted, false otherwise.
   * @protected
   * @final
   */
  isMounted: function(publicInstance) {
    return false;
  },
  /**
   * Forces an update. This should only be invoked when it is known with
   * certainty that we are **not** in a DOM transaction.
   *
   * You may want to call this when you know that some deeper aspect of the
   * component's state has changed but `setState` was not called.
   *
   * This will not invoke `shouldComponentUpdate`, but it will invoke
   * `componentWillUpdate` and `componentDidUpdate`.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {?function} callback Called after component is updated.
   * @param {?string} callerName name of the calling function in the public API.
   * @internal
   */
  enqueueForceUpdate: function(publicInstance, callback, callerName) {
    warnNoop(publicInstance, 'forceUpdate');
  },
  /**
   * Replaces all of the state. Always use this or `setState` to mutate state.
   * You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {object} completeState Next state.
   * @param {?function} callback Called after component is updated.
   * @param {?string} callerName name of the calling function in the public API.
   * @internal
   */
  enqueueReplaceState: function(
    publicInstance,
    completeState,
    callback,
    callerName,
  ) {
    warnNoop(publicInstance, 'replaceState');
  },
  /**
   * Sets a subset of the state. This only exists because _pendingState is
   * internal. This provides a merging strategy that is not available to deep
   * properties which is confusing. TODO: Expose pendingState or don't use it
   * during the merge.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {object} partialState Next partial state to be merged with state.
   * @param {?function} callback Called after component is updated.
   * @param {?string} Name of the calling function in the public API.
   * @internal
   */
  enqueueSetState: function(
    publicInstance,
    partialState,
    callback,
    callerName,
  ) {
    warnNoop(publicInstance, 'setState');
  },
};
export default ReactNoopUpdateQueue;

裏面提供了三個函數,分別是

  • enqueueForceUpdate: 強制更新隊列包裝器
  • enqueueReplaceState: 狀態替換隊列包裝器
  • enqueueSetState: 狀態更新隊列包裝器

實際裏面都是調用同一個方法warnNoop,設置首參數都同樣.

const didWarnStateUpdateForUnmountedComponent = {};

function warnNoop(publicInstance, callerName) {
  if (__DEV__) {
    const constructor = publicInstance.constructor;
    const componentName =
      (constructor && (constructor.displayName || constructor.name)) ||
      'ReactClass';
    const warningKey = `${componentName}.${callerName}`;
    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
      return;
    }
    warningWithoutStack(
      false,
      "Can't call %s on a component that is not yet mounted. " +
        'This is a no-op, but it might indicate a bug in your application. ' +
        'Instead, assign to `this.state` directly or define a `state = {};` ' +
        'class property with the desired state in the %s component.',
      callerName,
      componentName,
    );
    didWarnStateUpdateForUnmountedComponent[warningKey] = true;
  }
}

目測應該是給傳入的React組件實例設置componentName和KEY, 在裏面咱們再次看到同一個方法warningWithoutStack,咱們直接看看他究竟作了什麼. react/packages/shared/warningWithoutStack.js

/**
 * Similar to invariant but only logs a warning if the condition is not met.
 * This can be used to log issues in development environments in critical
 * paths. Removing the logging code for production environments will keep the
 * same logic and follow the same code paths.
 */

let warningWithoutStack = () => {};

if (__DEV__) {
  warningWithoutStack = function(condition, format, ...args) {
    if (format === undefined) {
      throw new Error(
        '`warningWithoutStack(condition, format, ...args)` requires a warning ' +
          'message argument',
      );
    }
    if (args.length > 8) {
      // Check before the condition to catch violations early.
      throw new Error(
        'warningWithoutStack() currently supports at most 8 arguments.',
      );
    }
    if (condition) {
      return;
    }
    if (typeof console !== 'undefined') {
      const argsWithFormat = args.map(item => '' + item);
      argsWithFormat.unshift('Warning: ' + format);

      // We intentionally don't use spread (or .apply) directly because it
      // breaks IE9: https://github.com/facebook/react/issues/13610
      Function.prototype.apply.call(console.error, console, argsWithFormat);
    }
    try {
      // --- Welcome to debugging React ---
      // This error was thrown as a convenience so that you can use this stack
      // to find the callsite that caused this warning to fire.
      let argIndex = 0;
      const message =
        'Warning: ' + format.replace(/%s/g, () => args[argIndex++]);
      throw new Error(message);
    } catch (x) {}
  };
}

export default warningWithoutStack;

相似invariant可是隻有不知足條件的時候纔會打印出警告.這能夠用於在關鍵路徑中記錄開發環境中的問題.生產環境下會移除日誌代碼保證正常邏輯.代碼只是一些基本的條件設定和優雅降級代碼.

還有一個相似的繼承類PureComponent,能夠用於組件進行淺對比決定是否須要更新

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

基本代碼和Component類似,也繼承自它的原型.但不繼承其自身的屬性方法.

相關文章
相關標籤/搜索