一、前置知识

在React17之前,我们写React代码的时候都会去引入React,并且自己的代码中没有用到,这是为什么呢?

这是因为我们的 JSX 代码会被 Babel 编译为 React.createElement,不引入 React 的话就不能使用React.createElement 了。我们来看一下babel的表示形式。
image.png

想要具体了解请移步https://www.yuque.com/linhe-8mnf5/fxyxkm/mtt8ho

自定义组件时需要首字母用大写,会被识别出是一个组件,这是一个规定。
小写默认会认为是一个html标签,编译成字符串。
image.png

二、入参解读

入参解读:创造一个元素需要知道哪些信息

  1. export function createElement(type, config, children)

createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。

  • type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
  • children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。
    1. React.createElement("ul", {
    2. // 传入属性键值对
    3. className: "list"
    4. // 从第三个入参开始往后,传入的参数都是 children
    5. }, React.createElement("li", {
    6. key: "1"
    7. }, "1"), React.createElement("li", {
    8. key: "2"
    9. }, "2"));
    对应的DOM结构
    1. <ul className="list">
    2. <li key="1">1</li>
    3. <li key="2">2</li>
    4. </ul>

1.React.createElement源码拆解

从入口文件React.js文件可知,React.createElement方法是从ReactElement文件引入进来的,我们就进入这个文件,定位到createElement方法。

image.png

1.1 先来看config参数的处理。

  1. // config 对象中存储的是元素的属性
  2. if (config != null) {
  3. // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
  4. if (hasValidRef(config)) {
  5. ref = config.ref;
  6. }
  7. // 此处将 key 值字符串化
  8. if (hasValidKey(config)) {
  9. key = '' + config.key;
  10. }
  11. self = config.__self === undefined ? null : config.__self;
  12. source = config.__source === undefined ? null : config.__source;
  13. // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
  14. for (propName in config) {
  15. if (
  16. // 筛选出可以提进 props 对象里的属性
  17. hasOwnProperty.call(config, propName) &&
  18. !RESERVED_PROPS.hasOwnProperty(propName)
  19. ) {
  20. props[propName] = config[propName];
  21. }
  22. }
  23. }

这段代码对 ref 以及 key 做了个验证处理,具体如何验证我们先不关心,从方法名称上来辨别一下,然后遍历 config 并把属性提进 props 对象里。

  1. const RESERVED_PROPS = {
  2. key: true,
  3. ref: true,
  4. __self: true,
  5. __source: true,
  6. };

也就是把ref和key剔除。

1.2 接下来是一段对于 children 的操作

  1. // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
  2. const childrenLength = arguments.length - 2;
  3. // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  4. if (childrenLength === 1) {
  5. // 直接把这个参数的值赋给props.children
  6. props.children = children;
  7. // 处理嵌套多个子元素的情况
  8. } else if (childrenLength > 1) {
  9. // 声明一个子元素数组
  10. const childArray = Array(childrenLength);
  11. // 把子元素推进数组里
  12. for (let i = 0; i < childrenLength; i++) {
  13. childArray[i] = arguments[i + 2];
  14. }
  15. // 最后把这个数组赋值给props.children
  16. props.children = childArray;
  17. }

首先把第二个参数之后的参数取出来,然后判断长度是否大于一。大于一的话就代表有多个 children,这时候 props.children 会是一个数组,否则的话只是一个对象。

1.3 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数

  1. // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  2. return ReactElement(
  3. type,
  4. key,
  5. ref,
  6. self,
  7. source,
  8. ReactCurrentOwner.current,
  9. props,
  10. );

1.4 处理传入的defaultProps

image.png

  1. // 处理 defaultProps
  2. if (type && type.defaultProps) {
  3. const defaultProps = type.defaultProps;
  4. for (propName in defaultProps) {
  5. if (props[propName] === undefined) {
  6. props[propName] = defaultProps[propName];
  7. }
  8. }
  9. }

2.小结

React.createElement源码流程图

image.png

createElement 中并没有十分复杂的涉及算法或真实 DOM 的逻辑,它的每一个步骤几乎都是在格式化数据。

说得更直白点,createElement 就像是开发者和 ReactElement 调用之间的一个“转换器”、一个数据处理层。它可以从开发者处接受相对简单的参数,然后将这些参数按照 ReactElement 的预期做一层格式化,最终通过调用 ReactElement 来实现元素的创建。整个过程如下图所示:

image.png

三、出参解读

image.png
上面已经分析过,createElement 执行到最后会 return 一个针对 ReactElement 的调用。

1.ReactElement源码拆解

  1. const ReactElement = function(type, key, ref, self, source, owner, props) {
  2. const element = {
  3. // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
  4. $$typeof: REACT_ELEMENT_TYPE,
  5. // 内置属性赋值
  6. type: type,
  7. key: key,
  8. ref: ref,
  9. props: props,
  10. // 记录创造该元素的组件
  11. _owner: owner,
  12. };
  13. //
  14. if (__DEV__) {
  15. // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
  16. }
  17. return element;
  18. };

$$typeof 来帮助我们识别这是一个 ReactElement

2.小结

ReactElement 其实只做了一件事情,那就是“创建”,说得更精确一点,是“组装”:ReactElement 把传入的参数按照一定的规范,“组装”进了 element 对象里,并把它返回给了 React.createElement,最终 React.createElement 又把它交回到了开发者手中。整个过程如下图所示:
image.png
可以在React中尝试打印:

  1. const AppJSX = (<div className="App">
  2. <h1 className="title">I am the title</h1>
  3. <p className="content">I am the content</p>
  4. </div>)
  5. console.log(AppJSX)

得到的控制台结果:
image.png

这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”

3.扩展知识

既然是“虚拟 DOM”,就意味着和渲染到页面上的真实 DOM 不是一个东西,那就需要用ReactDOM.render方法来渲染真实DOM。

  1. ReactDOM.render(
  2. // 需要渲染的元素(ReactElement)
  3. element,
  4. // 元素挂载的目标容器(一个真实DOM)
  5. container,
  6. // 回调函数,可选参数,可以用来处理渲染结束后的逻辑
  7. [callback]
  8. )

ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。比如,示例中的 App 组件,它对应的 render 调用是这样的:

  1. const rootElement = document.getElementById("root");
  2. ReactDOM.render(<App />, rootElement);

四、总结

源码拆解流程图:

image.png

整体流程:
image.png