源码结构
顶层目录
查看源码我们可以知道,这些才是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教学,我们调试源码遵循以下步骤:
- 从facebook/react项目main分支拉取最新源码
- 基于最新源码构建react、scheduler、react-dom三个包
- 通过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是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。 比如如下信息就不包括在JSX中:
- 组件在更新中的优先级
- 组件的state
- 组件被打上的用于Renderer的标记这些内容都包含在Fiber节点中。 所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。 在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记