源码项目结构

109 阅读5分钟

源码结构

顶层目录

image.png

查看源码我们可以知道,这些才是react的主要内容,而这些内容之中,最主要的还是packages。 下面说下packages下几个重要的包

react

React的核心,包含所有全局 React API,如:

  • React.createElement
  • React.Component
  • React.Children这些 API 是全平台通用的,它不包含ReactDOM、ReactNative等平台特定的代码。在 NPM 上作为单独的一个包发布。

scheduler

Scheduler(调度器)的实现,也是一个单独的包

shared

源码中其他模块公用的方法全局变量,比如在shared/ReactSymbols.js中保存React不同组件类型的定义。类似于我们平常项目中的constants和utils

Renderer相关的文件夹​

  • react-art

  • react-dom                 # 注意这同时是DOM和SSR(服务端渲染)的入口

  • react-native-renderer

  • react-noop-renderer       # 用于debug fiber(后面会介绍fiber)

  • react-test-renderer### 试验性包的文件夹 React将自己流程中的一部分抽离出来,形成可以独立使用的包,由于他们是试验性质的,所以不被建议在生产环境使用。包括如下文件夹:

  • react-server        # 创建自定义SSR流

  • react-client        # 创建自定义的流

  • react-fetch         # 用于数据请求

  • react-interactions  # 用于测试交互相关的内部特性,比如React的事件模型

  • react-reconciler    # Reconciler的实现,你可以用他构建自己的Renderer### 辅助包的文件夹 React将一些辅助功能形成单独的包。包括如下文件夹:

  • react-is       # 用于测试组件是否是某类型

  • react-client   # 创建自定义的流

  • react-fetch    # 用于数据请求

  • react-refresh  # “热重载”的React官方实现### react-reconciler文件夹 我们需要重点关注react-reconciler,在接下来源码学习中 80%的代码量都来自这个包。 虽然他是一个实验性的包,内部的很多功能在正式版本中还未开放。但是他一边对接Scheduler,一边对接不同平台的Renderer,构成了整个 React16 的架构体系。

调试源码

即使版本号相同(当前最新版为17.0.0 RC),但是facebook/react项目main分支的代码和我们使用create-react-app创建的项目node_modules下的react项目代码还是有些区别。 因为React的新代码都是直接提交到main分支,而create-react-app内的react使用的是稳定版的包。 为了始终使用最新版React教学,我们调试源码遵循以下步骤:

  1. 从facebook/react项目main分支拉取最新源码
  2. 基于最新源码构建react、scheduler、react-dom三个包
  3. 通过create-react-app创建测试项目,并使用步骤2创建的包作为项目依赖的包第一步肯定是克隆下来react main分支上的代码,安装依赖后,打包react、scheduler、react-dom三个包为dev环境可以使用的cjs包。(cjs包一般用于dev环境,用户用来开发的包)
yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE

现在源码目录build/node_modules下会生成最新代码的包。我们为react、react-dom创建yarn link。

cd build/node_modules/react
# 申明react指向
yarn link
cd build/node_modules/react-dom
# 申明react-dom指向
yarn link

然后我们就用cra创建一个react项目,然后link到我们刚才打出的包中

yarn link react react-dom

现在试试在react/build/node_modules/react-dom/cjs/react-dom.development.js中随意打印些东西。这边建议放置在其他文件夹中,不然每次编辑的时候很费劲。

深入理解jsx

JSX作为描述组件内容的数据结构,为JS赋予了更多视觉表现力。在React中我们大量使用他。在深入源码之前,有些疑问我们需要先解决:

  • JSX和Fiber节点是同一个东西么?
  • React Component、React Element是同一个东西么,他们和JSX有什么关系?JSX在编译时会被Babel编译为React.createElement方法。所以写了jsx语法的地方,必须加上 import React from 'react';否则在运行时该模块内就会报未定义变量 React的错误。尽管在react17中,已经不需要再写这种声明了。 JSX并不是只能被编译为React.createElement方法,你可以通过@babel/plugin-transform-react-jsx插件显式告诉Babel编译时需要将JSX编译成什么函数的调用(默认为React.createElement)。比如在preact这个类React库中,JSX会被编译为一个名为h的函数调用,再例如vue,但是不一样的是,它是相当于是原来的fork, babel-plugin-transform-vue-jsx 插件。

react.createElement

我们把createElement省略一下:

export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 将 config 处理后赋值给 props
    // ...省略
  }

  const childrenLength = arguments.length - 2;
  // 处理 children,会被赋值给props.children
  // ...省略

  // 处理 defaultProps
  // ...省略

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};

React.createElement最终会调用ReactElement方法返回一个包含组件数据的对象,该对象有个参数$$typeof: REACT_ELEMENT_TYPE标记了该对象是个React Element。 所以调用React.createElement返回的对象就是React Element么? React提供了验证合法React Element的全局API React.isValidElement(opens new window),我们看下他的实现:

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

可以看到,$$typeof === REACT_ELEMENT_TYPE的非null对象就是一个合法的React Element。换言之,在React中,所有JSX在运行时的返回结果(即React.createElement()的返回值)都是React Element。

react-component

在React中,我们常使用ClassComponent与FunctionComponent构建组件。

class AppClass extends React.Component {
  render() {
    return <p>KaSong</p>
  }
}
console.log('这是ClassComponent:', AppClass);
console.log('这是Element:', <AppClass/>);


function AppFunc() {
  return <p>KaSong</p>;
}
console.log('这是FunctionComponent:', AppFunc);
console.log('这是Element:', <AppFunc/>);

ClassComponent对应的Element的type字段为AppClass自身。 FunctionComponent对应的Element的type字段为AppFunc自身,如下所示:

{
  $$typeof: Symbol(react.element),
  key: null,
  props: {},
  ref: null,
  type: ƒ AppFunc(),
  _owner: null,
  _store: {validated: false},
  _self: null,
  _source: null 
}

但是又因为

AppClass instanceof Function === true;
AppFunc instanceof Function === true;

所以无法通过引用类型区分ClassComponent和FunctionComponent。React通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent。

jsx和fiber

从上面的内容我们可以发现,JSX是一种描述当前组件内容的数据结构,他不包含组件schedulereconcilerender所需的相关信息。 比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer的标记这些内容都包含在Fiber节点中。 所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记