React16源码: JSX2JS及React.createElement源码实现

JSX 到 Javascript 的转换

  • React中的 JSX 类似于 Vue中的template模板文件,Vue是基于编译时将template模板转换成render函数
  • 在React中,JSX是类似于html和javascript混编的语法,而javascript是真的javascript, html并非真的html
  • 它的可阅读性可维护性都是要高很多的

1 )JSX2JS 原理

  • JSX 通过 babel 进行转换之后,生成了纯JS
    • JSX相对于JS来讲,它唯一的一个区别,就是它可以写类似于HTML的一个标签
    • 比如说我们通过写div 然后在 div 这种方式去声明HML的标签
    • 然后它会给我们返回在React当中需要使用的对象
  • 这就是JSX到JS的一个转化过程

2 ) 工具演示

  • 这个工具是 babel playground
    • babeljs.io/repl
  • 要做一些代码转化的一个测试,可以直接到这个playground上面来
    • 它会实时在为我们展现出我们写的代码转化出来的是什么样的样子
    • 下面的示例是使用较低版本的 babel 来配合 React 16.6 版本

示例1

jsx

<div></div>

js

React.createElement("div", null);

示例2

jsx

<div class="test"></div>

js

React.createElement("div", {
  class: "test"
});

示例3

jsx

<div id="div" key="key" class="test">
  <span>1</span>
  <span>1</span>
</div>

js

React.createElement("div", {
  id: "div",
  key: "key",
  class: "test"
}, React.createElement("span", null, "1"), React.createElement("span", null, "1"));

示例4

jsx

function Comp() {
  return <a>123</a>
}

<Comp id="div" key="key">
  <span>1</span>
  <span>1</span>
  <div id="box">
  	<span class='inner'>2</span>
  </div>
</Comp>

js

function Comp() {
  return React.createElement("a", null, "123");
}

React.createElement(Comp, {
  id: "div",
  key: "key"
}, 
	React.createElement("span", null, "1"),
	React.createElement("span", null, "1"),
	React.createElement("div", {
	  id: "box"
	}, 
		React.createElement("span", {
		  class: "inner"
		}, "2")
	)
);

3 )说明

  • 从上面我们可以看出来我们的一个类似html的标签,或者是组件的一个标签
  • 通过这种尖括号的方式来写的,它最终都会转换成 React.createElement
  • 我们写的这些标签或者一些props,或者它的 children 都会作为一个参数
    • props 是一个 key-value 形式的一个对象
    • 它可以支持多层,无限层的嵌套,也就是一个树形结构
  • 如果是一个函数式的组件作为参数
    • 这里要分两种情况
      • 1 ) 组件是大写的,这样会直接转换成变量(对象)
      • 2 ) 组件是小写的,这样会直接变成字符串类型的标记(组件将失效)
    • 注意
      • 如果变成字符串,那么在React中,它是会认为这是一个原生的dom节点的
      • 如果不存在这么一个dom节点,那么后续在运行的时候,可能就报错了
      • 所以自定义的组件必须使用大写的开头,这是一个规范
  • 综上,我们现在问题的重点就在 createElement 之上了

React.createElement 源码解析

  • 在上一步的 JSX2JS中,我们的标签,标签里的属性,标签的内容,都会变成各种类型的参数
  • 传到我们调用的 createElement 这个方法里面,这个方法内部如何实现的
  • 在 createElement 函数的内部,返回了一个 React Element, 我们来看看它具体的作用
  • 看源码肯定要从它的入口文件开始看,因为入口文件会给我们很多的信息告诉我们
  • 常用的使用这个包的时候的这些API它都来自于哪里,以及它是如何 export 出来的

1 )React 入口文件 packages/react/src/React.js

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import ReactVersion from 'shared/ReactVersion';
import {
  REACT_CONCURRENT_MODE_TYPE,
  REACT_FRAGMENT_TYPE,
  REACT_PROFILER_TYPE,
  REACT_STRICT_MODE_TYPE,
  REACT_SUSPENSE_TYPE,
} from 'shared/ReactSymbols';

import {Component, PureComponent} from './ReactBaseClasses';
import {createRef} from './ReactCreateRef';
import {forEach, map, count, toArray, only} from './ReactChildren';
import {
  createElement,
  createFactory,
  cloneElement,
  isValidElement,
} from './ReactElement';
import {createContext} from './ReactContext';
import {lazy} from './ReactLazy';
import forwardRef from './forwardRef';
import memo from './memo';
import {
  createElementWithValidation,
  createFactoryWithValidation,
  cloneElementWithValidation,
} from './ReactElementValidator';
import ReactSharedInternals from './ReactSharedInternals';
import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,
  lazy,
  memo,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  Suspense: REACT_SUSPENSE_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,

  version: ReactVersion,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
};

if (enableStableConcurrentModeAPIs) {
  React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
  React.Profiler = REACT_PROFILER_TYPE;
} else {
  React.unstable_ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
  React.unstable_Profiler = REACT_PROFILER_TYPE;
}

export default React;
  • 上面全是 import 其他一些东西,import 进来之后, 它声明了 React 对象
  • 这个对象就是我们在外部去用 React 的时候,给我们提供的API
  • 然后最终它 export default React 把这个对象给它 export 出来
  • 这样的话我们就可以在外部使用
  • 我们回到 createElement 上面来,从上面可知,跟Element相关的一些代码
  • 都放在了 ./ReactElement 这个文件下面

2 )定位到 ReactElement.js 文件中

/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';

import ReactCurrentOwner from './ReactCurrentOwner';

const hasOwnProperty = Object.prototype.hasOwnProperty;

const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};

let specialPropKeyWarningShown, specialPropRefWarningShown;

function hasValidRef(config) {
  if (__DEV__) {
    if (hasOwnProperty.call(config, 'ref')) {
      const getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  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;
}

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;
  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,
  });
}

/**
 * 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 {*} key
 * @param {string|object} ref
 * @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.
 * @param {*} owner
 * @param {*} props
 * @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;
};

/**
 * 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;

  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,
  );
}

/**
 * Return a function that produces ReactElements of a given type.
 * See https://reactjs.org/docs/react-api.html#createfactory
 */
export function createFactory(type) {
  const factory = createElement.bind(null, type);
  // Expose the type on the factory and the prototype so that it can be
  // easily accessed on elements. E.g. `.type === Foo`.
  // This should not be named `constructor` since this may not be the function
  // that created the element, and it may not even be a constructor.
  // Legacy hook: remove it
  factory.type = type;
  return factory;
}

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );

  return newElement;
}

/**
 * Clone and return a new ReactElement using element as the starting point.
 * See https://reactjs.org/docs/react-api.html#cloneelement
 */
export function cloneElement(element, config, children) {
  invariant(
    !(element === null || element === undefined),
    'React.cloneElement(...): The argument must be a React element, but you passed %s.',
    element,
  );

  let propName;

  // Original props are copied
  const props = Object.assign({}, element.props);

  // Reserved names are extracted
  let key = element.key;
  let ref = element.ref;
  // Self is preserved since the owner is preserved.
  const self = element._self;
  // Source is preserved since cloneElement is unlikely to be targeted by a
  // transpiler, and the original source is probably a better indicator of the
  // true owner.
  const source = element._source;

  // Owner will be preserved, unless ref is overridden
  let owner = element._owner;

  if (config != null) {
    if (hasValidRef(config)) {
      // Silently steal the ref from the parent.
      ref = config.ref;
      owner = ReactCurrentOwner.current;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    // Remaining properties override existing props
    let defaultProps;
    if (element.type && element.type.defaultProps) {
      defaultProps = element.type.defaultProps;
    }
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        if (config[propName] === undefined && defaultProps !== undefined) {
          // Resolve default props
          props[propName] = defaultProps[propName];
        } else {
          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];
    }
    props.children = childArray;
  }

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

/**
 * Verifies the object is a ReactElement.
 * See https://reactjs.org/docs/react-api.html#isvalidelement
 * @param {?object} object
 * @return {boolean} True if `object` is a ReactElement.
 * @final
 */
export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}
  • 在这个文件里面先找到 createElement 这个方法,我们可以看到它接收的三个参数
    • type
      • 就是我们的节点类型,如果是原生的节点,那么它是一个字符串
      • 那如果是我们自己声明的组件,它就是一个class component 或者是一个 functional component
      • 还会有其他的一些情况。比如 使用 React 原生的一些组件
        • 比如说 Fragment、 StrictMode、 Suspense
        • 这些都是 React 提供我们的一些原生组件,
        • 其实,上面三个它们默认就只是一个 Symbol
        • 它没有任何其他的功能,就仅仅是一个标志
    • config
      • 是我们写在这个JSX标签上面的所有的 attributes
      • 它们都会变成key value的形式存到这个config对象里面
      • 我们要从这个对象里面筛选出真正的props的内容
      • 还有特殊的,比如说 key,ref 这些属性
    • children
      • 就是我们标签中间我们放的一些内容
      • 它可能是一个子标签,或者它直接是文字 text
  • 我们来看一下它如何去创建一个 ReactElement, 还是回到 createElement 这个方法
    • 内部声明了一堆变量,找到有没有合理的REF,有没有合理的key
    • 我们把这些都给它读到一个单独的变量里面
    • 忽略 self 和 source 这两个东西,不是特别的重要
    • 接下去,要对剩下的config下面的 props 进行一个处理
      • 判断一下它是否是内建的 props,如果不是的话,就放到一个新建的 props 对象里面
      • 如果是内建的 props,就不放进去了,因为它不属于正常的 props 的范畴
      • 看一下这个内建的 props,它是什么东西
        const RESERVED_PROPS = {
          key: true,
          ref: true,
          __self: true,
          __source: true,
        };
        
      • key, ref, __self, __source, 这些都不会出现在我们使用class component 的场景下
        • 比如说我们的 this.props里面
        • 因为在处理props的过程当中,就已经把它处理掉了
    • 这边把 props 的属性全部拿出来,放到一个新的对象里面之后
    • 接下去要处理children
      • children 是可以有多个的,在一个节点下面
      • 它的 children 可能有很多的兄弟节点存在
      • 它是作为后续的参数传进来的,虽然在声明 createElement的时候只有三个参数
      • 但是它是可以传 3,4,5,6,7,8,9, … 多个参数的
      • 后续第三个参数之后的参数,我们都认为它们是 children
      • 在react当中把后续 arguments.length - 2 代表剩下的这个长度都是children
      • 然后会一个一个把它读出来,然后变成一个数组
      • 声明一个数组, 存放后续所有的 children 节点对象, 最终再把它放到 props.children
      • 通过 this.props.children 拿到的就是这部分的内容
    • 接下来,就是 defaultProps 的处理
      • 在声明 class Comp 的时候,比如说我们 extends React.Component
      • 我们可以通过 Comp.defaultProps 一个对象,给接收的这些props去设置一些默认值
      • 比如说, 这边它的默认值是 {value:1},当组件在被使用的时候,没有传value这个props
      • 在这里面就会使用 1 作为我们在组件内部 this.props.value 去拿到的这个值
      • 它就是把我们刚才上面处理过的那个props对象上面去读取对应的defaultProps里面的每一个key的值
      • 如果值是 undefined,就把它设置为 defaultProps 里面的属性
      • 如果它是有值的, 我们就不设置了
      • 注意
        • 它的判断条件是 undefined
        • 也就是说 null 也是一个不需要使用默认值的情况
    • 接着,下面 DEV 判断的代码,进行忽略
    • 最终 return了一个 ReactElement
      • 传入刚才处理过的这些内容
  • 关于 ReactElement
    • 它不是一个 class Comp, 而是一个 function
    • 最终会return一个Object, 这个Object也就几个主要的属性
      • $$typeofREACT_ELEMENT_TYPE
        • 是用来标识我们的 element 是什么类型的
        • 在写JSX代码的时候,所有的节点都是通过 createElement 进行创建的
        • 那么,它的 $$typeof 永远都是 REACT_ELEMENT_TYPE
        • 在后续React的更新渲染dom的过程中是经常被用到的
        • 大部分情况下,我们拿到的 $$typeof 都是 REACT_ELEMENT_TYPE
        • 有一些特殊情况是和平台相关
          • 在react-dom里面,它有一个API叫做 React.createPortal, 它返回的对象和这里的类似
          • 但是它的 $$typeofREACT_PORTAL_TYPE
      • type 是之前传进来的那个 type
        • 是在 createElement 的时候接收的那个 type
        • 用于记录节点的类型,是原生组件,还是 class Comp
      • key 就是上面处理过的 key
      • ref 就是 ref
      • props 就是 props
    • 综上,就是一个 ReactElement, 具体的方法,如何去操作,以及最终返回的类型

你可能感兴趣的:(React,React,Native,react.js,javascript,前端)