资讯专栏INFORMATION COLUMN

React系列 --- createElement, ReactElement与Component部

xuhong / 2339人阅读

摘要:仅针对数据属性描述有效获取该属性的访问器函数。当且仅当指定对象的属性可以被枚举出时,为。冻结及其对象主要目的是为提高测试环境下效率,将的一些属性配置为不可枚举,进行遍历的时候跳过这些属性。

React系列

React系列 --- 简单模拟语法(一)
React系列 --- Jsx, 合成事件与Refs(二)
React系列 --- virtualdom diff算法实现分析(三)
React系列 --- 从Mixin到HOC再到HOOKS(四)
React系列 --- createElement, ReactElement与Component部分源码解析(五)
React系列 --- 从使用React了解Css的各种使用方案(六)

前言

因为之前写过一些文章分别关于怎么模拟React语法,React基本知识和virtualdom diff实现思路,接下来就跟着React源码大概了解一下怎么一个过程,只是主逻辑代码,忽略部分开发环境的代码.
以下解析仅限于我当时理解,不一定准确.

JSX编译

还是用之前的例子

123456

编译成

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

我们看下API的语法

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

创建并返回给定类型的新 React element 。

参数 描述
type 既可以是一个标签名称字符串,也可以是一个 React component 类型(一个类或一个函数),或者一个React fragment 类型
props 各种属性值
children 子元素

因为有babel会编译JSX,所以一般很少会直接调用这个方法.

然后我们进入找到对应的源码位置查看代码 react/packages/react/src/ReactElement.js

/**
 * 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方法使用,如果有部分代码还不知道是干嘛的话也不用担心,下面会有说到

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的取值器,使用时会执行对应方法进行报错不让读取.

至此相关源码基本了解了

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 

Hello, {this.props.name}

; } }

从源码看这个类写法做了什么 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相似,也继承自它的原型.但不继承其自身的属性方法.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106499.html

相关文章

  • React 源码深度解读(一):首次DOM元素渲染 - Part 1

    摘要:调用栈是这样的这里生成的我们将其命名为,它将作为参数传入到。整个的调用栈是这样的组件间的层级结构是这样的到此为止,顶层对象已经构造完毕,下一步就是调用来自的方法,进行页面的渲染了。通过表达的结构最终会转化为一个纯对象,用于下一步的渲染。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言...

    daydream 评论0 收藏0
  • React源码系列一之createElement

    前言:使用react也有二年多了,一直停留在使用层次。虽然很多时候这样是够了。但是总觉得不深入理解其背后是的实现逻辑,很难体会框架的精髓。最近会写一些相关的一些文章,来记录学习的过程。 备注:react和react-dom源码版本为16.8.6 本文适合使用过React进行开发,并有一定经验的人阅读。 好了闲话少说,我们一起来看源码吧写过react知道,我们使用react编写代码都离不开webpa...

    Zhuxy 评论0 收藏0
  • React 源码深度解读(四):首次自定义组件渲染 - Part 1

    摘要:本篇开始介绍自定义组件是如何渲染的。组件将自定义组件命名为,结构如下经过编译后,生成如下代码构建顶层包装组件跟普通元素渲染一样,第一步先会执行创建为的。调用顺序已在代码中注释。先看图,这部分内容将在下回分解 前言 React 是一个十分庞大的库,由于要同时考虑 ReactDom 和 ReactNative ,还有服务器渲染等,导致其代码抽象化程度很高,嵌套层级非常深,阅读其源码是一个非...

    Warren 评论0 收藏0
  • 从头实现一个简易版React(一)

    摘要:既然看不懂,那就看看社区前辈们写的一些源码分析文章以及实现思路吧,又这么过了几天,总算是摸清点思路,于是在参考了前辈们的基础上,实现了一个简易版的。总结以上就是实现一个的总体思路,下节我们重点放在不同的上。 写在开头 工作中使用react也很长一段时间了,虽然对它的用法,原理有了一定的了解,但是总感觉停留在表面。本着知其然知其所以然的态度,我试着去看了react源码,几天下来,发现并不...

    meislzhua 评论0 收藏0
  • 剖析 React 源码:先热个身

    摘要:我们先来看下这个函数的一些神奇用法对于上述代码,也就是函数来说返回值是。不管你第二个参数的函数返回值是几维嵌套数组,函数都能帮你摊平到一维数组,并且每次遍历后返回的数组中的元素个数代表了同一个节点需要复制几次。这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章: 现在工作中基本都用 React 了,由此想了解下内部原理 市面上 Vue 的源码解读数不胜数,但是反观...

    sean 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<