源码概览

本节将对 React 的源码架构,约定及其实现进行概述。

如果您想参与 React 的开发,我们希望这份指南可以帮助你更加轻松地进行修改。

我们并不推荐在 React 应用中遵循这些约定。有许多约定是历史原因,并且之后也许会有所修改。

项目根目录

当克隆 React 仓库之后,你们将看到项目根目录的信息:

  • packages 包含元数据(比如 package.json)和 React 仓库中所有 package 的源码(子目录 src)。如果你需要修改源代码, 那么每个包的 src 子目录是你最需要花费精力的地方。
  • fixtures 包含一些给贡献者准备的小型 React 测试项目。
  • build 是 React 的输出目录。源码仓库中并没有这个目录,但是它会在你克隆 React 并且第一次构建它之后出现。

文档位于 React 仓库之外的一个独立仓库中

还有一些其他的顶层目录,但是它们几乎都是工具类的,并且在贡献代码时基本不会涉及。

共置测试

我们没有单元测试的顶层目录。而是将它们放置在所需测试文件的相同目录下的 __tests__ 的目录之中。

比如,一个用于 setInnerHTML.js 的测试文件,会存放在 __tests__/setInnerHTML-test.js,就在它同级目录下。

warning 和 invariant

React 源码采用 warning 模块展示警告。

var warning = require('warning');

warning(
  2 + 2 === 4,
  'Math is not working today.'
);

警告会在 warning 的条件为 false 时出现。

warning 机制可以理解为,当编写判定条件的时候,应当使用符合正常逻辑的条件判断,这样出现异常的时候就会触发 warning,注意判定条件不要用反了。

我们应当注意避免大量打印重复的 warning:

var warning = require('warning');

var didWarnAboutMath = false;
if (!didWarnAboutMath) {
  warning(
    2 + 2 === 4,
    'Math is not working today.'
  );
  didWarnAboutMath = true;
}

warning 仅在开发环境中启用。在生产环境中,他们会被完全剔除掉。如果你需要在生产环境禁止执行某些代码,请使用 invariant 模块代替 warning

var invariant = require('invariant');

invariant(
  2 + 2 === 4,
  'You shall not pass!'
);

invariant 判别条件为 false 时,会将 invariant 的信息作为错误抛出

“Invariant” 用于声明 “这个条件应总为 true”。你可以把它当成一种断言。

保持开发和生产环境的行为相似是十分重要的,因此 invariant 在开发和生产环境下都会抛出错误。不同点在于在生产环境中这些错误信息会被自动替换成错误代码,这样可以让输出库文件变得更小。

开发环境与生产环境

你可以在代码库中使用 __DEV__ 这个伪全局变量,用于管理开发环境中需运行的代码块

这在编译阶段会被内联,在 CommonJS 构建中,转化成 process.env.NODE_ENV !== 'production' 这样的判断。

对于独立构建来说,在没有 minify 的构建中,它会变成 true,同时在 minify 的构建中,检测到的 if 代码块会被完全剔除。

if (__DEV__) {
  // 仅在开发环境下执行的代码
}

Flow

我们最近将 Flow 引入源码,用于类型检查。在许可证头部的注释中,标记为 @flow 注释的文件是已经经过类型检查的。

我们接受添加 Flow 注释到现有代码。Flow 注释看上去像这样:

ReactRef.detachRefs = function(
  instance: ReactInstance,
  element: ReactElement | string | number | null | false,
): void {
  // ...
}

如果可以的话,新代码应尽量使用 Flow 注释。 你可以运行 yarn flow,用 Flow 本地检查你的代码。

动态注入

React 在一些模块中使用了动态注入。虽然它总是显式地,但仍然存在问题,因为这会阻碍对代码的理解。它存在的最主要原因是 React 原本只以支持 DOM 为目标。然而 React Native 开始作为 React 的一个分支之后。我们只好添加一些动态注入让 React Native 覆盖一些行为。

你可能看到过一些模块,像下面这样声明动态依赖:

// 动态注入
var textComponentClass = null;

// 依赖动态注入的值
function createInstanceForText(text) {
  return new textComponentClass(text);
}

var ReactHostComponent = {
  createInstanceForText,

  // 提供动态注入的入口
  injection: {
    injectTextComponentClass: function(componentClass) {
      textComponentClass = componentClass;
    },
  },
};

module.exports = ReactHostComponent;

injection 字段并没有用某种特别的方式处理。但是按照惯例,这意味着这模块在运行时想要注入一些(很可能是平台特定的)依赖。

在代码库中有许多注入点。未来,我们打算抛弃动态注入机制,并且在构建的时候静态地连接所有的碎片。

Multiple Packages

React 采用 monorepo 的管理方式。仓库中包含多个独立的包,以便于更改可以一起联调,并且问题只会出现在同一地方。

React Core

React “Core” 中包含所有全局 React API,比如:

  • React.createElement()
  • React.Component
  • React.Children

React 核心只包含定义组件必要的 API。它不包含协调算法或者其他平台特定的代码。它同时适用于 React DOM 和 React Native 组件。

React 核心代码在源码的 packages/react 目录中。在 npm 上发布为 react 包。相应的独立浏览器构建版本称为 react.js,它会导出一个称为 React 的全局对象。

渲染器

React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。

渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。

渲染器同样位于 packages/ 目录下:

另外一个官方支持的渲染器的是 react-art。它曾经是一个独立的 GitHub 仓库,但是现在我们将此加入了主源代码树。

注意:

严格说来,react-native-renderer 实现了 React 和 React Native 的连接。真正渲染 Native 视图的平台特定代码及组件都存储在 React Native 仓库中。

reconcilers

即便 React DOM 和 React Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。

为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState() 这样的更新时,reconciler 会调用树中组件上的 render(),然后决定是否进行挂载,更新或是卸载操作。

Reconciler 没有单独的包,因为他们暂时没有公共 API。相反,它们被如 React DOM 和 React Native 的渲染器排除在外。

Stack reconciler

“stack” reconciler 是 React 15 及更早的解决方案。虽然我们已经停止了对它的使用, 但是这在下一章节有详细的文档。

Fiber reconciler

“fiber” reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。

它的主要目标是:

  • 能够把可中断的任务切片处理。
  • 能够调整优先级,重置并复用任务。
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界。

你可以在这里这里,深入了解 React Fiber 架构。虽然这已经在 React 16 中启用了,但是 async 特性还没有默认开启。

源代码在 packages/react-reconciler 目录下。

事件系统

React 实现一个合成事件,这与渲染器无关,它适用于 React DOM 和 React Native。源码在 packages/legacy-events 目录下。

这个是一个深入研究事件系统代码的视频(66分钟)。

下一章节学习什么?

查看下一章节去学习 reconciler 在 pre-React 16 中的实现。我们还没有为新的 reconciler 内部原理编写文档。