美文网首页
react.createElement方法探究

react.createElement方法探究

作者: anddju | 来源:发表于2020-05-14 19:09 被阅读0次

前言:今天偶有闲暇,突然想知道 虚拟DOM 是如何实现的,说到 虚拟DOM ,便不得不说说 createElement 方法。所以先看 createElement 方法,并于此记录一波。(下次在写这种源码指不定啥时候)

目前看的版本: 16.13.1

看源码准备工作

第一步:clone项目

第二步,搜索:createElement, 搜索结果中,唯一能和定义沾边的就是这个 React.d.ts 文件了

搜索结果.png

第三步:根据 React.d.ts,找到当前目录的 src,接着找 React.jsts语法我还没学过,不过看到 .ts,我就去找根目录下的同名文件,一般都能找到)。 看到这熟悉的声明语句,我就知道我找到了 。

const createElement = __DEV__ ? createElementWithValidation : createElementProd;

开始正式查看

createElement 方法在 开发模式中使用 createElementWithValidation,在生产模式中使用 createElementProd。(开发者模式和生产模式的区分变量 __DEV__,为啥叫这个,一会我研究研究在发一篇文章,头一次考虑这么细)

一般在开发者模式中,注释和代码缩进都会很不错,所以看开发者模式的方法 createElementWithValidation

createElementWithValidation, 顾名思义 创建_元素_经过_校验。具体怎么校验和创建,咱们接着往下看。 (这个方法是相对引用的文件,怎么找我就不说了)

createElementWithValidation 方法第一句。玩呢。。上来就是一函数。。 先看名字 isValidElementType,盲猜:是_有效_元素_类型, 点击去瞅瞅。

isValidElementType.png

得嘞,看完第一行的这个函数,咱们接着往下走。
为了方便,我直接在代码注释内写理解了

  // We warn in this case but don't throw. We expect the element creation to
  // succeed and there will likely be errors in render.
  /***
    在这种情况下我们会警告但不要抛出错误。我们希望元素创建成功,并且呈现中可能会有错误。
  **/
  if (!validType) {  /*** 如果没有经过前面的类型校验 **/
    let info = '';
 /*** 如果是个 undefined 元素,或者 是个空对象, 我们就提示他:

您可能忘记从定义组件的文件中导出组件,或者您可能混淆了默认导入和命名导入。(不得不说这个提示是真的友好)
 **/
    if (
      type === undefined ||
      (typeof type === 'object' &&
        type !== null &&
        Object.keys(type).length === 0)
    ) {
      info +=
        ' You likely forgot to export your component from the file ' +
        "it's defined in, or you might have mixed up default and named imports.";
    }

    /*** 获取出错文件路径 或者出错组件名称,方便查找, 查找路径的这个方法后边再看,跟我目标虚拟DOM 不是很沾边 **/
    const sourceInfo = getSourceInfoErrorAddendumForProps(props);
    if (sourceInfo) {
      /*** 如果存在引用信息,就提示出错路径和出错行数 **/
      info += sourceInfo;
    } else {
      /*** 没有引用,那就提示出错组件名称 **/
      info += getDeclarationErrorAddendum();
    }
    

  /*** 判断组件 类型,分为 null ,array, 自定义组件, 原生组件 **/
    let typeString;
    if (type === null) {
      typeString = 'null';
    } else if (Array.isArray(type)) {
      typeString = 'array';
    } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) {
      typeString = `<${getComponentName(type.type) || 'Unknown'} />`;
      info =
        ' Did you accidentally export a JSX literal instead of a component?';
    } else {
      typeString = typeof type;
    }

    /*** 如果是开发模式,就拼接错误,并输出 **/
    if (__DEV__) {
      console.error(
        'React.createElement: type is invalid -- expected a string (for ' +
          'built-in components) or a class/function (for composite ' +
          'components) but got: %s.%s',
        typeString,
        info,
      );
    }
  }

OK,第一大段落,错误验证就算结束了。接下来请看第二段,生成 element 元素

  /*** 创建元素,一个大函数,容后再解释 **/
  const element = createElement.apply(this, arguments);

  // The result can be nullish if a mock or a custom function is used.
 /*** 如果使用mock或自定义函数,则结果可能为空。 **/
  // TODO: Drop this when these are no longer allowed as the type argument.
/*** 当不再允许这些参数作为类型参数时,删除此项。 **/
  if (element == null) {
/*** 如果是 mock 或 自定义函数时, 返回 null元素 **/
    return element;
  }

  // Skip key warning if the type isn't valid since our key validation logic
  // doesn't expect a non-string/function type and can throw confusing errors.
  // We don't want exception behavior to differ between dev and prod.
  // (Rendering will throw with a helpful message and as soon as the type is
  // fixed, the key warnings will appear.)
/***翻译
如果类型无效,则跳过密钥警告,因为我们的密钥验证逻辑不需要非字符串/函数类型,并且可能会引发混淆错误。
我们不希望生产模式和开发模式的异常处理有所不同。
(提示一条有帮助的消息,在类型被修复之前,将显示关键警告。)
 **/
  if (validType) {
    for (let i = 2; i < arguments.length; i++) {
      /*** 函数大意:
确保每个元素要么在静态位置传递,要么在定义了显式键属性的数组中传递,要么在具有有效键属性的对象文本中传递。 **/
      validateChildKeys(arguments[i], type);
    }
  }

/*** 校验Props 有效性, 分为 fragment标签和 普通标签 **/
  if (type === REACT_FRAGMENT_TYPE) {
    validateFragmentProps(element);
  } else {
    validatePropTypes(element);
  }

/*** 元素创建完毕,返回元素 **/
  return element;

接下来,看重点。 createElement 方法 所在文件packages/react/src/ReactElement.js

首先,函数部分上来就是这些声明,这些声明也标志了一个 react组件 需要的参数

  /***  props 名称 **/
let propName;

/*** 提取props ,放到 props变量内 **/
  // Reserved names are extracted
  const props = {};

  let key = null;  /*** 元素 key 值 **/
  let ref = null; /*** 元素ref **/
  let self = null;  /*** 元素上一级 **/
  let source = null; /*** 元素路径 **/

校验 ref

/*** 存在 ref ,并且有效  ,函数内 `Object.getOwnPropertyDescriptor(config, 'ref').get;`写的贼棒 **/
if (hasValidRef(config)) {
      ref = config.ref;

      if (__DEV__) {
        /*** 开发模式中,如果ref 无效,提示的内容,应该是新版才会用的 useRef 和 createRef **/
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }

校验 key

 if (hasValidKey(config)) {
      key = '' + config.key;
    }

selfsource 进行校验,赋值

self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;

props 属性处理,利用遍历和赋值

// Remaining properties are added to a new props object
    for (propName in config) {
    /*** hasOwnProperty = Object.prototype.hasOwnProperty;  可遍历属性
   RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};   没有这些属性
**/
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }

迁移 children

/*** 翻译
子项可以是多个参数,并且这些子项被转移到新分配的props对象上。
 **/
// 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];
    }
    props.children = childArray;
  }

生成并返回 react元素

return ReactElement(element.type, key, ref, self, source, owner, props);

接下来是讲解 ReactElement 方法。 packages/react/src/ReactElement.js

找到 ReactElement 方法之后,首先看到的就是这一大串注释
我感觉挺有用,先翻译一波。

/***
 * 方法介绍翻译:
     一个用于创造 react元素的 格式化(???这么说应该没啥毛病)方法。
     这里不再遵循class声明,请不要使用 new 来调用它。
     另外,检查某个元素是否为react元素也不可以使用 instanceof 。
      可以使用 $$typeof 是否为  Symbol.for('react.element')。
 *
 * @param {*} type
 * @param {*} props
 * @param {*} key
 * @param {string|object} ref
 * @param {*} owner
 * @param {*} self 当调用React.createElement时,一个*临时*助手,用于检测“this”与“owner”是否不同,以便我们发出警告。我们希望去掉owner并用箭头函数替换字符串ref,只要this和owner相同,行为就不会改变。
 * @param {*} source 表示文件名、行号或其它信息的注释对象(由transpiler或其它方式添加)。
 * @internal
 **/

接下来看正式的代码,整个代码只有一个声明,一个返回,所以只看声明就OK了。

const element = {
    /*** 翻译: 这个标签允许我们将其唯一地标识为一个React元素 **/
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,
     /*** REACT_ELEMENT_TYPE = Symbol.for('react.element') **/

    /*** 翻译: 属于元素的内置属性 **/
    // 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,
  };
/*** 中间有一堆开发模式才有代码,这个先无视,下面再讲 **/
return element;

看到这里,有点轻微懵逼,竟然不是直接返回元素,返回的竟然是个对象,没有任何dom节点返回,设想:这里返回的对象,在经过框架的某个函数,将变成 真实的dom。

稍微提一下属性修饰词: MDN

  1. 形参1:obj : 要定义属性的对象。
  2. 形参2:prop :要定义或修改的属性的名称或 Symbol
  3. 形参3:desc:要定义或修改的属性描述符。
    | desc描述 | 描述 | 默认值|
    | ----------- | ----------- | -----------|
    | configurable | 该属性的描述符才能够被改变,同时该属性是否可以被删除 | false |
    | enumerable | 是否可枚举,遍历的时候是否显示这个属性 | false |
    | value | 该属性的值 | undefined |
    | writable | value值是否可以被改变 | false|

看一下开发模式才有的这些属性修饰词吧

if (__DEV__) {
    /*** 翻译:
验证标志现在是可变的。我们把它放在一个外部后备存储器上,这样我们就可以冻结整个对象。一旦它们在常用的开发环境中执行(难道是在说变化?),就可以用WeakMap来代替它们。
 **/
    // 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 = {};

    /*** 翻译: 
为了使比较ReactElements更容易用于测试,我们将验证标志设为不可枚举(在可能的情况下,它应该包括我们运行测试的每个环境),因此测试框架忽略它。
**/
    // 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属性:不可枚举,不可删除,不可赋值 **/
    // 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,
    });
    /*** 如果可以,冻结 元素和 元素的 props 属性 **/
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

相关文章

网友评论

      本文标题:react.createElement方法探究

      本文链接:https://www.haomeiwen.com/subject/zsitohtx.html