React学习十一天---React源码之准备工作和JSX是怎么转化为ReactElement的(一)

967 阅读9分钟

项目源码

前言

前面我们学习了React核心算法,现在我们接下来阅读React的源码

  • 配置React源码本地调试环境
  • JSX转换为ReactElement的过程
  • React检测开发者是否错误的使用了props属性

1. 配置React源码本地调试环境

我们要查看React核心代码,一定会遇到一些不理解的地方,这时候我们就要对源码进行调试。通过运行源码来查看结果,看结果是否与我们想要的结果是否一致。所以在阅读代码之前,我们要搭建一个源码调试环境,这个源码环境并不是通过脚手架直接创建一个React项目而已,因为脚手架内部使用的react源码,和官方放在github的代码是有差别的。我们要克隆官方github的源码再结合脚手架来搭建调试源码环境。下面我们按照具体步骤完成环境搭建。

  1. 使用 create-react-app脚手架创建一个react项目
npx create-react-app react-test
  1. 弹射create-react-app脚手架内部配置
npm run eject
  1. 克隆 react 官方源码 在项目的更目录下进行克隆
git clone --branch v16.13.1 --depth=1 git@github.com:facebook/react.git src/react

image.png

react目录里面就是官方源码

  1. 链接本地源码
   // 文件位置: react-test/config/webpack.config.js
   alias: {
       "react-native": "react-native-web",
       "react": path.resolve(__dirname, "../src/react/packages/react"),
       "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"),
       "shared": path.resolve(__dirname, "../src/react/packages/shared"),
       "react-reconciler": path.resolve(__dirname, "../src/react/packages/react-reconciler"),
       "legacy-events": path.resolve(__dirname, "../src/react/packages/legacy-events")
     }

找到resolve配置项替换掉alias

image.png

  1. 修改环境变量
   // 文件位置: react-test/config/env.js
   const stringified = {
   	"process.env": Object.keys(raw).reduce((env, key) => {
      	env[key] = JSON.stringify(raw[key])
         return env
      }, {}),
      __DEV__: true,
      SharedArrayBuffer: true,
      spyOnDev: true,
      spyOnDevAndProd: true,
      spyOnProd: true,
      __PROFILE__: true,
      __UMD__: true,
      __EXPERIMENTAL__: true,
      __VARIANT__: true,
      gate: true,
      trustedTypes: true
    }

image.png

  1. 告诉babel在转化代码时忽略类型检查

    npm install @babel/plugin-transform-flow-strip-types -D

    // 文件位置: react-test/config/webpack.config.js [babel-loader]
    plugins: [
      require.resolve("@babel/plugin-transform-flow-strip-types"),
    ]
    

image.png 7. 导出 HostConfig

// 文件位置: /react/packages/react-reconciler/src/ReactFiberHostConfig.js
+ export * from './forks/ReactFiberHostConfig.dom';
- invariant(false, 'This module must be shimmed by a specific renderer.');

image.png

  1. 修改 ReactSharedInternals.js 文件

    // 文件位置: /react/packages/shared/ReactSharedInternals.js
    - import * as React from 'react';
    - const ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
    + import ReactSharedInternals from '../react/src/ReactSharedInternals';
    

image.png

  1. 关闭 eslint 扩展

    // 文件位置: react/.eslintrc.js [module.exports]
    // 删除 extends
    extends: [
      'fbjs',
      'prettier'
    ]
    
    

image.png

  1. 禁止 invariant 报错

    // 文件位置: /react/packages/shared/invariant.js
    export default function invariant(condition, format, a, b, c, d, e, f) {
      if (condition) return;
      throw new Error(
        'Internal React error: invariant() is meant to be replaced at compile ' +
          'time. There is no runtime version.',
      );
    }
    

image.png

  1. eslint 配置

    在 react 源码文件夹中新建 .eslintrc.json 并添加如下配置

    {
      "extends": "react-app",
      "globals": {
        "SharedArrayBuffer": true,
        "spyOnDev": true,
        "spyOnDevAndProd": true,
        "spyOnProd": true,
        "__PROFILE__": true,
        "__UMD__": true,
        "__EXPERIMENTAL__": true,
        "__VARIANT__": true,
        "gate": true,
        "trustedTypes": true
      }
    }
    
  2. 修改 react react-dom 引入方式

    import * as React from "react"
    import * as ReactDOM from "react-dom"
    

image.png

  1. 解决 vsCode 中 flow 报错

    "javascript.validate.enable": false
    

image.png

  1. 可选项配置

    如果你的 vscode 编辑器安装了 prettier 插件并且在保存 react 源码文件时右下角出现如下错误,按照如下步骤解决

image.png 1. 全局安装 prettier

   `npm i prettier -g`

2. 配置 prettier path

   Settings > Extensions > Prettier > Prettier path

   <img src="./images/2.png" width="80%" align="left"/>

image.png 15. __DEV__ 报错

删除 node_modules 文件夹,执行 npm install

image.png

image.png 运行的是源码console.log('test'),说明配置成功!!!

2. JSX转换为ReactRElement的过程

首先我们要通过阅读源码搞清楚,JSX是如何被转换为React元素(即VirtualDOM),用js对象来描述真实DOM的样子。在React当中我们使用JSX用来描述用户界面的的构建,JSX看起来很像html,但实际上是JavaScript的一种扩展,本质还是JavaScript。但是JSX语法是不能直接在浏览器上运行的。在React代码在浏览器运行之前呢,JSX被Babel编译为React.createElement方法的调用,createElement方法在调用后返回的就是ReactElement,就是virtualDOM。所以我们要在源码中找到createElement这个方法。看一看这个方法内部是怎样实现的。

2.1 找到createElement方法

我们找到源码入口文件,package里面就是react的所有源码,我们找到react目录,查看他的package.json发现入口文件是index.js 我们找到index.js文件

image.png

发现代码在src/React image.png 再找,发现在ReactElement.js文件中 image.png 在这个文件中我们搜索function createElement,就可以找到我们需要的createElement方法,nice!!!

image.png

2.2 解析createElement

这个方法就是用来创建 React Element的,具体做了四件事

  1. 分离 props 属性和特殊属性
  2. 将子元素挂载到 props.children中
  3. 为 props 属性赋默认值值
  4. 创建并返回 ReactELement

1. 传入三个参数

  • type
    • 代表元素类型,div span,或者组件
  • config(配置属性)
    • 包含了元素的props属性,还包含了react源码需要的一些特殊属性(比如ref属性,或者key属性),如果这个元素有属性呢config就是一个对象,如果没有config值为null
  • children
    • 代表当前元素的子元素

2. 分离 props 属性和特殊属性

export function createElement(type, config, children) {
 /**
  * propName -> 属性名称
  * 用于后面的 for 循环
  */
 let propName;
 /**
  * 存储 React Element 中的普通元素属性 即不包含 key ref self source
  */
 const props = {};
 
  /**
  * 待提取属性
  * React 内部为了实现某些功能而存在的属性
  */
 let key = null;
 let ref = null;
 let self = null;
 let source = null;
 ...
 }

propName 开始声明的propName变量,用于后面两次的for...in...循环,在循环的时候需要建一个属性变量,如果我在每次循环中都新建一个变量,这样在性能上有一个损耗的,如果我只用一个propName用于后面的两次for...in...循环,就稍微的提升了性能。(大佬写代码这么抠,细思极恐)

props 这个变量存储的就是元素的普通属性,从形参config中分离出来

key, ref, self, source这四个变量都是从config中分离出来的React 元素的特殊属性,React为了实现某些功能而存在的属性

接下来要做的就是将react 元素属性放入props中,将特殊属性放入key, ref, self, source四个变量当中。

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

export function createElement(type, config, children) {
 ···
 // 如果 config 不为 null
 if (config != null) {
   // 如果 config 对象中有合法的 ref 属性
   if (hasValidRef(config)) {
     // 将 config.ref 属性提取到 ref 变量中
     ref = config.ref;
     // 在开发环境中
     if (__DEV__) {
       // 如果 ref 属性的值被设置成了字符串形式就报一个提示
       // 说明此用法在将来的版本中会被删除
       warnIfStringRefCannotBeAutoConverted(config);
     }
   }
   // 如果在 config 对象中拥有合法的 key 属性
   if (hasValidKey(config)) {
     // 将 config.key 属性中的值提取到 key 变量中
     key = '' + config.key;
   }

   self = config.__self === undefined ? null : config.__self;
   source = config.__source === undefined ? null : config.__source;
   // 遍历 config 对象
   for (propName in config) {
     // 如果当前遍历到的属性是对象自身属性
     // 并且在 RESERVED_PROPS 对象中不存在该属性
     if (
       hasOwnProperty.call(config, propName) &&
       !RESERVED_PROPS.hasOwnProperty(propName)
     ) {
       // 将满足条件的属性添加到 props 对象中 (普通属性)
       props[propName] = config[propName];
     }
   }
 }
 ...
 }

if(config != null) 在这个里面进行props和特殊属性分离:

  • if (hasValidRef(config)) 收集ref(里面出现if (DEV) 友好告诉开发者,这个属性有可能会被废除)
  • if (hasValidKey(config)) 收集key
  • self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source;selfsource特殊属性的收集
  • for (propName in config) 很明了,是对props的收集 RESERVED_PROPS 收集的是特殊属性,如果config属性满足在config对象中存在且不是特殊属性则将他们收集到props属性里面。

3. 将子元素挂载到 props.children中

. 将子元素挂载到 props.children中 主要逻辑:

  • 将第三个及之后的参数挂载到 props.children 属性中
  • 如果子元素是多个 props.children 是数组
  • 如果子元素是一个 props.children 是对象
export function createElement(type, config, children) {
  // 由于从第三个参数开始及以后都表示子元素
  // 所以减去前两个参数的结果就是子元素的数量
  const childrenLength = arguments.length - 2;
  // 如果子元素的数量是 1
  if (childrenLength === 1) {
    // 直接将子元素挂载到到 props.children 属性上
    // 此时 children 是对象类型
    props.children = children;
    // 如果子元素的数量大于 1
  } else if (childrenLength > 1) {
    // 创建数组, 数组中元素的数量等于子元素的数量
    const childArray = Array(childrenLength);
    // 开启循环 循环次匹配子元素的数量
    for (let i = 0; i < childrenLength; i++) {
      // 将子元素添加到 childArray 数组中
      // i + 2 的原因是实参集合的前两个参数不是子元素
      childArray[i] = arguments[i + 2];
    }
    // 如果是开发环境
    if (__DEV__) {
      // 如果 Object 对象中存在 freeze 方法
      if (Object.freeze) {
        // 调用 freeze 方法 冻结 childArray 数组
        // 防止 React 核心对象被修改 冻结对象提高性能
        Object.freeze(childArray);
      }
    }
    // 将子元素数组挂载到 props.children 属性中
    props.children = childArray;
  }
  }

4.为 props 属性赋默认值值

主要逻辑:

如果当前处理是组件 看组件身上是否有 defaultProps 属性 这个属性中存储的是 props 对象中属性的默认值 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined 如果为undefined 就将默认值赋值给对应的 props 属性值

export function createElement(type, config, children) {
···
    // 将 type 属性值视为函数 查看其中是否具有 defaultProps 属性
   if (type && type.defaultProps) {
     // 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量
     const defaultProps = type.defaultProps;
     // 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量
     for (propName in defaultProps) {
       // 如果 props 对象中的该属性的值为 undefined
       if (props[propName] === undefined) {
         // 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值
         props[propName] = defaultProps[propName];
       }
     }
   }
···
}

5. 创建并返回 ReactELement

// 返回 ReactElement
return ReactElement(
  type,
  key,
  ref,
  self,
  source,
  // 在 Virtual DOM 中用于识别自定义组件
  ReactCurrentOwner.current,
  props,
);

最后返回一个 ReactElement方法调用,这个方法很简单结束了对应的参数,将这些参数赋值给一个对象进行返回。

const ReactElement = function (type, key, ref, self, source, owner, props) {
const element = {
  
  $$typeof: REACT_ELEMENT_TYPE,
  type: type,
  key: key,
  ref: ref,
  props: props,
  _owner: owner,
};
···
}

$$typeof 组件的类型, 十六进制数值或者 Symbol 值 React 在最终在渲染 DOM 的时候, 需要确保元素的类型是 REACT_ELEMENT_TYPE需要此属性作为判断的依据

type 元素具体的类型值 如果是元素节点 type 属性中存储的就是 div span 等等 如果元素是组件 type 属性中存储的就是组件的构造函数

key 元素的唯一标识 用作内部 vdom 比对 提升 DOM 操作性能

ref存储元素 DOM 对象或者组件 实例对象

props存储向组件内部传递的数据

_owner记录当前元素所属组件 (记录当前元素是哪个组件创建的)

6. React检测开发者是否错误的使用了props属性

在开发环境中 React 会检测开发者是否在组件内部 通过 props 对象获取 key 属性或者 ref 属性 如果开发者调用了 在控制台中报错误提示

看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件 如果元素不是组件 就直接返回元素类型字符串 displayName 用于在报错过程中显示是哪一个组件报错了 如果开发者显式定义了 displayName 属性 就显示开发者定义的 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'

function defineKeyPropWarningGetter(props, displayName) {
// 通过 props 对象获取 key 属性报错
const warnAboutAccessingKey = function () {
 // 在开发环境中
 if (__DEV__) {
   // specialPropKeyWarningShown 控制错误只输出一次的变量
   if (!specialPropKeyWarningShown) {
     // 通过 specialPropKeyWarningShown 变量锁住判断条件
     specialPropKeyWarningShown = true;
     // 指定报错信息和组件名称
     console.error(
       '%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://reactjs.org/link/special-props)',
       displayName,
     );
   }
 }
};
warnAboutAccessingKey.isReactWarning = true;
// 为 props 对象添加 key 属性
Object.defineProperty(props, 'key', {
 // 当获取 key 属性时调用 warnAboutAccessingKey 方法进行报错
 get: warnAboutAccessingKey,
 configurable: true,
});
}
// 如果处于开发环境
if (__DEV__) {
 // 元素具有 key 属性或者 ref 属性
 if (key || ref) {
   
   const displayName =
     typeof type === 'function'
       ? type.displayName || type.name || 'Unknown'
       : type;
   // 如果 key 属性存在
   if (key) {
     // 为 props 对象添加key 属性
     // 并指定当通过 props 对象获取 key 属性时报错
     defineKeyPropWarningGetter(props, displayName);
   }
   // 如果 ref 属性存在
   if (ref) {
     // 为 props 对象添加 ref 属性
     // 并指定当通过 props 对象获取 ref 属性时报错
     defineRefPropWarningGetter(props, displayName);
   }
 }
}

7. isValidElement方法的内部实现

验证 object 参数是否是 ReactElement. 返回布尔值

验证成功的条件:

  • object 是对象

  • object 不为 null

  • object 对象中的 $$typeof 属性值为 REACT_ELEMENT_TYPE

    export function isValidElement(object) {
       return (
         typeof object === 'object' &&
         object !== null &&
         object.$$typeof === REACT_ELEMENT_TYPE
       );
     }
    
    

总结

好了,今天就暂且到这里,下面我来简单总结下今天的要点。

我们配置完成了一套可以查看且可以调试的官方github中的react源码环境,为后面的源码学习做好准备。 还学习了React里面将JSX转换为ReactRElement的过程

  • 分离 props 属性和特殊属性
  • 将子元素挂载到 props.children 中
  • 为 props 属性赋默认值 (defaultProps)
  • 创建并返回 ReactElement

这里用一个变量在两次循环中使用,节省一点点内存,性能有些些优化,有相应场景值得学习。

最后还分析了开发时,开发者使用key 和 ref代码实现,使用Object.defineProperty进行属性监听,并且使用锁specialPropRefWarningShown机制只进行一次提示。