likes
comments
collection
share

React源码系列(四):JSX解析

作者站长头像
站长
· 阅读数 23

前言

这是React源码系列专栏的第四篇文章,预计写10篇左右,之前的文章请查看文末,通过本专栏的学习,相信大家可以快速掌握React源码的相关概念以及核心思想,向成为大佬的道路上更近一步; 本章我们学习 createElement 解析,本课程源码基于v18.2.0版本;

JSX

在React17之前,我们写React代码的时候都会去引入React,并且自己的代码中没有用到,这是为什么呢? 这是因为我们的 JSX 代码会被 Babel 编译为 React.createElement,我们来看一下babel的表示形式。 React源码系列(四):JSX解析 需要注意的是:

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

React源码系列(四):JSX解析 结论:JSX 的本质是React.createElement这个 JavaScript 调用的语法糖。是JS的语法扩展;

React17之后的版本 React 已经不需要引入 createElement ,这种模式来源于 Automatic Runtime,看一下是如何编译的。

function App(){
  return <div>
    <h1>hello,world</h1>
  </div>
}

被编译后的文件:

import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function App() {
  return  _jsxs("div", {
            children: [
                _jsx("h1", {
                   children: "hello,world"
                }),
            ],
        });
}

plugin-syntax-jsx 已经向文件中提前注入了 _jsxRuntime api。不过这种模式下需要我们在 .babelrc 设置 runtime: automatic 。

"presets": [    
  ["@babel/preset-react",{
    "runtime": "automatic"
  }]     
],

createElement源码阅读

从上面我们知道jsx通过babel编译成React.createElement,下面我们就去看一下相关源码。 1、首先我们先找到 createElement 暴露位置,位于 packages/react/index 目录下;

// packages/react/index
export { createElement } from './src/React';

2、进入 packages/react/src/React 找到 createElement引入位置;

// packages/react/src/React.js
import {
  createElement as createElementProd,
} from './ReactElement';

3、进入packages/react/src/ReactElement.js 找到 createElement 函数;

// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  // ... 省略 ...
}

入参解读

createElement源码位于 react/packages/react/src/ReactElement.js 入参解读:创造一个元素需要知道哪些信息

export function createElement(type, config, children) { /* ... 省略 ... */}

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

  • type:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型。
  • config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。
  • children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”。

我们将createElement 操作拆分为以下四步:

1、config参数处理

// config 对象中存储的是元素的属性
if (config != null) { 
  // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
  if (hasValidRef(config)) {
    ref = config.ref;
  }
  // 此处将 key 值字符串化
  if (hasValidKey(config)) {
    key = '' + config.key; 
  }
  self = config.__self === undefined ? null : config.__self;
  source = config.__source === undefined ? null : config.__source;
  // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
  for (propName in config) {
    if (
      // 筛选出可以提进 props 对象里的属性
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName) 
    ) {
      props[propName] = config[propName]; 
    }
  }
}

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

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

2、对 children 的操作

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

// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
  const childrenLength = arguments.length - 2; 
  // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  if (childrenLength === 1) { 
    // 直接把这个参数的值赋给props.children
    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
    props.children = childArray; 
  }

3、处理传入的defaultProps

defaultProps主要处理默认值相关操作

// 处理 defaultProps
if (type && type.defaultProps) {
  const defaultProps = type.defaultProps;
  for (propName in defaultProps) { 
    if (props[propName] === undefined) {
      props[propName] = defaultProps[propName];
    }
  }
}

4、ReactElement

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

// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );

5、完整代码

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 (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      if (__DEV__) {
        checkKeyStringCoercion(config.key);
      }
      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,
  );
}

createElement小结

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

出参解读

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

const ReactElement = function(type, key, ref, self, source, owner, props) { /*省略*/}

ReactElement源码拆解

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录创造该元素的组件
    _owner: owner,
  };

  // 
  if (__DEV__) {
    // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
  }

  return element;
};

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

ReactElement小结

ReactElement 其实只做了一件事情就是组装数据。 可以在React中尝试打印:

const AppJSX = (<div className="App">
  <h1 className="title">I am the title</h1>
  <p className="content">I am the content</p>
</div>)

console.log(AppJSX)

得到的控制台结果: React源码系列(四):JSX解析 这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是虚拟 DOM;

小结

本章我们学习了 createElement 的源码,接下来的文章将进入 React 主流程分析,欢迎继续跟随本专栏一起学习;

React源码系列

转载自:https://juejin.cn/post/7200571354928250941
评论
请登录