Bootstrap

React核心源码解读 - 学习笔记

React核心源码解读 - 学习笔记

文章出处: 拉 勾 教 育 大前端 高薪训练营

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

  1. 使用 create-react-app 脚手架创建项目

    npx create-react-app react-test

  2. 弹射 create-react-app 脚手架内部配置

    npm run eject

  3. 克隆 react 官方源码 (在项目的根目录下进行克隆)

    git clone --branch v16.13.1 --depth=1 https://github.com/facebook/react.git src/react

  4. 链接本地源码

    // 文件位置: react-test/config/webpack.config.js
    resolve: {
         
      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")
      }
    }
    
  5. 修改环境变量

    // 文件位置: 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
     }
    
  6. 告诉 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"),
    ]
    
  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.');
    
  8. 修改 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';
    
  9. 关闭 eslint 扩展

    // 文件位置: react/.eslingrc.js [module.exports]
    // 删除 extends
    extends: [
      'fbjs',
      'prettier'
    ]
    
    
  10. 禁止 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.',
      );
    }
    
  11. 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
      }
    }
    
  12. 修改 react react-dom 引入方式

    import * as React from "react"
    import * as ReactDOM from "react-dom"
    
  13. 解决 vsCode 中 flow 报错

    "javascript.validate.enable": false
    
  14. 可选项配置

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

    1. 全局安装 prettier

      npm i prettier -g

    2. 配置 prettier path

      Settings > Extensions > Prettier > Prettier path
      在这里插入图片描述

  15. __DEV__ 报错

    删除 node_modules 文件夹,执行 npm install

2. 创建 React 元素

JSX 被 Babel 编译为 React.createElement 方法的调用,createElement 方法在调用后返回的就是 ReactElement,就是 virtualDOM。

2.1 createElement

文件位置:packages/react/src/ReactElement.js

/**
 * 创建 React Element
 * type      元素类型
 * config    配置属性
 * children  子元素
 * 1. 分离 props 属性和特殊属性
 * 2. 将子元素挂载到 props.children 中
 * 3. 为 props 属性赋默认值 (defaultProps)
 * 4. 创建并返回 ReactElement
 */
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;

  // 如果 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];
      }
    }
  }

  /**
   * 将第三个及之后的参数挂载到 props.children 属性中
   * 如果子元素是多个 props.children 是数组
   * 如果子元素是一个 props.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;
  }

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

  // 将 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];
      }
    }
  }

  /**
   * 在开发环境中 如果元素的 key 属性 或者 ref 属性存在
   * 监测开发者是否在组件内部通过 props 对象获取了 key 属性或者 ref 属性
   * 如果获取了 就报错
   */

  // 如果处于开发环境
  if (__DEV__) {
   
    // 元素具有 key 属性或者 ref 属性
    if (key || ref) {
   
      // 看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件
      // 如果元素不是组件 就直接返回元素类型字符串
      // displayName 用于在报错过程中显示是哪一个组件报错了
      // 如果开发者显式定义了 displayName 属性 就显示开发者定义的
      // 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'
      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);
      }
    }
  }
  // 返回 ReactElement
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    // 在 Virtual DOM 中用于识别自定义组件
    ReactCurrentOwner.current,
    props,
  );
}

2.2 ReactElement

文件位置:packages/react/src/ReactElement.js

/**
 * 接收参数 返回 ReactElement
 */
const ReactElement = function (type, key, ref, self, source, owner, props) {
   
  const element = {
   
    /**
     * 组件的类型, 十六进制数值或者 Symbol 值
     * React 在最终在渲染 DOM 的时候, 需要确保元素的类型是 REACT_ELEMENT_TYPE
     * 需要此属性作为判断的依据
     */
    $$typeof: REACT_ELEMENT_TYPE,

    /**
     * 元素具体的类型值 如果是元素节点 type 属性中存储的就是 div span 等等
     * 如果元素是组件 type 属性中存储的就是组件的构造函数
     */
    type: type,
    /**
     * 元素的唯一标识
     * 用作内部 vdom 比对 提升 DOM 操作性能
     */
    key: key,
    /**
     * 存储元素 DOM 对象或者组件 实例对象
     */
    ref: ref,
    /**
     * 存储向组件内部传递的数据
     */
    props: props,

    /**
     * 记录当前元素所属组件 (记录当前元素是哪个组件创建的)
     */
    _owner: owner,
  };
  // 返回 ReactElement
  return element;
};

2.3 hasValidRef

文件位置:packages/react/src/ReactElement.js

/**
 * 查看参数对象中是否有合法的 ref 属性
 * 返回布尔值
 */
function hasValidRef(config) {
   
  return config.ref !== undefined;
}

2.4 hasValidKey

文件位置:packages/react/src/ReactElement.js

/**
 * 查看参数对象中是否有合法的 key 属性
 * 返回布尔值
 */
function hasValidKey(config) {
   
  return config.key !== undefined;
}

2.5 isValidElement

文件位置:packages/react/src/ReactElement.js

/**
 * 验证 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
  );
}

2.6 defineKeyPropWarningGetter

文件位置:packages/react/src/ReactElement.js

/**
 *  指定当通过 props 对象获取 key 属性时报错
 *  props        组件中的 props 对象
 *  displayName  组件名称标识
 */
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,
  });
}

2.7 defineRefPropWarningGetter

文件位置:packages/react/src/ReactElement.js

/**
 *  指定当通过 props 对象获取 ref 属性时报错
 *  props        组件中的 props 对象
 *  displayName  组件名称标识
 */
function defineRefPropWarningGetter(props, displayName) {
   
  // 通过 props 对象获取 ref 属性报错
  const warnAboutAccessingRef = function () {
   
    if (__DEV__) {
   
      // specialPropRefWarningShown 控制错误只输出一次的变量
      if (!specialPropRefWarningShown) {
   
        // 通过 specialPropRefWarningShown 变量锁住判断条件
        specialPropRefWarningShown = true;
        // 指定报错信息和组件名称
        console.error(
          '%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://reactjs.org/link/special-props)',
          displayName,
        );
      }
    }
  };

  warnAboutAccessingRef.isReactWarning = true;
  // 为 props 对象添加 key 属性
  Object.defineProperty(props, 'ref', {
   
    get: warnAboutAccessingRef,
    configurable: true,
  });
}

3. React 架构

React 16 版本的架构可以分为三层:调度层、协调层、渲染层。

  • Scheduler (调度层):调度任务的优先级,高优任务优先进入协调器
  • Reconciler (协调层):构建 Fiber 数据结构,比对 Fiber 对象找出差异, 记录 Fiber 对象要进行的 DOM 操作
  • Renderer (渲染层):负责将发生变化的部分渲染到页面上

3.1 Scheduler 调度层

在 React 15 的版本中,采用了循环加递归的方式进行了 virtualDOM 的比对,由于递归使用 JavaScript 自身的执行栈,一旦开始就无法停止,直到任务执行完成。如果 VirtualDOM 树的层级比较深,virtualDOM 的比对就会长期占用 JavaScript 主线程,由于 JavaScript 又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。

在 React 16 的版本中,放弃了 JavaScript 递归的方式进行 virtualDOM 的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了 virtualDOM 比对造成页面卡顿的问题。

在 window 对象中提供了 requestIdleCallback API,它可以利用浏览器的空闲时间执行任务,但是它自身也存在一些问题,比如说并不是所有的浏览器都支持它,而且它的触发频率也不是很稳定,所以 React 最终放弃了 requestIdleCallback 的使用。

在 React 中,官方实现了自己的任务调度库,这个库就叫做 Scheduler。它也可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,高优先级任务先执行,低优先级任务后执行。

Scheduler 存储在 packages/scheduler 文件夹中。

3.2 Reconciler 协调层

在 React 15 的版本中,协调器和渲染器交替执行,即找到了差异就直接更新差异。在 React 16 的版本中,这种情况发生了变化,协调器和渲染器不再交替执行。协调器负责找出差异,在所有差异找出之后,统一交给渲染器进行 DOM 的更新。也就是说协调器的主要任务就是找出差异部分,并为差异打上标记。

3.3 Renderer 渲染层

渲染器根据协调器为 Fiber 节点打的标记,同步执行对应的DOM操作。

既然比对的过程从递归变成了可以中断的循环,那么 React 是如何解决中断更新时 DOM 渲染不完全的问题呢?

其实根本就不存在这个问题,因为在整个过程中,调度器和协调器的工作是在内存中完成的是可以被打断的,渲染器的工作被设定成不可以被打断,所以不存在DOM 渲染不完全的问题。

4. 数据结构

4.1 Fiber

type Fiber = {
   
  /************************  DOM 实例相关  *****************************/
  
  // 标记不同的组件类型, 值详见 WorkTag
  tag: WorkTag,

  // 组件类型 div、span、组件构造函数
  type: any,

  // 实例对象, 如类组件的实例、原生 dom 实例, 而 function 组件没有实例, 因此该属性是空
  stateNode: any,
 
	/************************  构建 Fiber 树相关  ***************************/
  
  // 指向自己的父级 Fiber 对象
  return: Fiber | null,

  // 指向自己的第一个子级 Fiber 对象
  child: Fiber | null,
  
  // 指向自己的下一个兄弟 iber 对象
  sibling: Fiber | null,
  
  // 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
  // 我们称他为 current <==> workInProgress
  // 在渲染完成之后他们会交换位置
  // alternate 指向当前 Fiber 在 workInProgress 树中的对应 Fiber
	alternate: Fiber | null,
		
  /************************  状态数据相关  ********************************/
  
  // 即将更新的 props
  pendingProps: any, 
  // 旧的 props
  memoizedProps: any,
  // 旧的 state
  memoizedState: any,
		
  /************************  副作用相关 ******************************/

  // 该 Fiber 对应的组件产生的状态更新会存放在这个队列里面 
  updateQueue: UpdateQueue<any> | null,
  
  // 用来记录当前 Fiber 要执行的 DOM 操作
  effectTag: SideEffectTag,

  // 存储要执行的 DOM 操作
  firstEffect: Fiber | null,
  
  // 单链表用来快速查找下一个 side effect
  nextEffect: Fiber | null,
  
  // 存储 DOM 操作完后的副租用 比如调用生命周期函数或者钩子函数的调用
  lastEffect: Fiber | null,

  // 任务的过期时间
  expirationTime: ExpirationTime,
  
	// 当前组件及子组件处于何种渲染模式 详见 TypeOfMode
  mode: TypeOfMode,
};

在这里插入图片描述

4.2 WorkTag

文件位置:packages/shared/ReactWorkTags.js

type WorkTag =
  | 0
  | 1
  | 2
  | 3
  | 4
  | 5
  | 6
  | 7
  | 8
  | 9
  | 10
  | 11
  | 12
  | 13
  | 14
  | 15
  | 16
  | 17
  | 18
  | 19
  | 20
  | 21
  | 22;

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2;
export const HostRoot = 3;
export const HostPortal = 4;
export const HostComponent = 5<
;