搭建React源码本地调试环境
既然要阅读、调试源码,过程中肯定会出现一些不理解的地方,这时候就需要对源码进行调试,通过运行源码来查看结果与我们预计的结果是否一致,所以在阅读源码之前要先搭建一个源码调试环境。
这个环境并不是通过脚手架工具简单的创建一个React项目。因为脚手架内部使用的React源码与官方放在GitHub上的源码是有差别的,所以要直接克隆GitHub中的源码,再结合脚手架工具来搭建一个本地的源码调试环境。
相关步骤及命令如下:
// 1、使用create-react-app脚手架创建项目
npx create-react-app react-source
// 2、弹射create-react-app脚手架内部配置(运行此命令需要先初始化git 仓库)
npm run eject
// 3、克隆react官方源码(在项目的根目录进行克隆)
git clone --branch v16.13.1 --depth=1 https://github.com/facebook/react.git src/react
// 4、链接本地源码 将react-source/config/webpack.config.js中 resolve下的alias替换为:
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-source/config/env.js
// 用以下内容替换原文件的stringified
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
// 在webpack.config.js添加babel-loader的plugin
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 specifix 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/.eslintrc.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引入方式 主要是src/index.js和src/App.js
import * as React from 'react'
import * as ReactDOM from 'react-dom'
// 13、解决vscode中flow报错
"javascript.validate.enable": false
// 14、可选配置(vscode 安装了prettier插件并在保存react源码文件时右下角出现错误)
// 解决方法:
npm i prettier -g
// vscode中
Settings > Extensions > Prettier > Prettier path 修改path路径即可
// 15、如果有__DEV__报错
删除node_modules文件夹,重新执行npm install
JSX转为ReactElement的过程
- 分离props属性和特殊属性。
如果传入的config不为空,则先处理ref属性和key属性,在循环遍历处理剩下的普通属性。 - 将子元素挂载到props.children中。
获取传入子元素的数量,如果大于1则转为数组并循环赋值,如果为1则直接赋值 - 给props属性赋默认值。
如果defaultProps存在,则遍历defaultProps,判断传入的属性是否有值,如果没有则用defaultProps中的值替换 - 创建并返回ReactElement。
调用ReactElement,其实就是将参数放在一个对象中返回了 相关源码如下:
/**
* @description: 创建React Element元素
* 1、分离props属性和特殊属性
* 2、将子元素挂在到props.children上
* 3、为props属性赋值默认值 defaultProps
* 4、创建并返回 ReactElement
* @param {String} type 元素类型
* @param {Object} config 配置属性 props ref key等属性
* @param {Array} children 子节点
* @return {Object} ReactElement
*/
export function createElement(type, config, children) {
// 属性名称
// 用于后面的for循环,否则在循环中创建变量会损耗性能
let propName;
// Reserved names are extracted
// 存储 React Element 中的普通元素属性,即不包含key ref source self
const props = {};
// 特殊属性
let key = null;
let ref = null;
let self = null;
let source = null;
// 如果config不为空,则分离特殊属性
if (config != null) {
// 是否有ref属性
if (hasValidRef(config)) {
ref = config.ref;
// 在开发环境
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
// 如果有key
if (hasValidKey(config)) {
key = '' + config.key;
}
// 判断self和source属性
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
// 遍历config对象
for (propName in config) {
// 如果当前遍历到的属性是对象自身属性
// 并且在RESERVED_PROPS对象中不存在这个属性
// 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;
} else if (childrenLength > 1) {
// 如果大于1 则转为数组
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
// 处理默认值
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 开发环境
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
// 检测有没有通过props获取key属性
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
React16版本架构
React16版本的架构可以分为三层:调度层、协调层、渲染层。
- Scheduler(调度层):调度任务的优先级,高优任务优先进入协调器。
- Reconciler(协调层):构建Fiber数据结构,对比Fiber对象找出差异,记录Fiber对象要进行的DOM操作。
- Renderer(渲染层):负责将发生变化的部分渲染到页面上。
Scheduler调度层
在React15版本中,采用了循环加递归的方式进行了VirtualDom的比对.由于递归会使用JavaScript自身的执行栈,一旦开始就无法中断,直到任务完成。如果VirtualDom树的层级比较深,VirtualDom的比对就会长期占用JavaScript的主线程,由于JavaScript又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。
在React16版本中,加入了调度层,所有任务不是直接被执行,而是加入到任务队列中,等浏览器空闲时才会执行。 而且React16版本放弃了JavaScript递归的方式进行VirtualDom的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,没有空闲时间则会被中断,不会长期占用主线程,这就解决了VirtualDom比对造成页面卡顿的问题。
在window对象中提供了requestIdleCallback API,它可以利用浏览器的空闲时间执行任务,但是它自身也存在一些问题,比如说并不是所有的浏览器都支持,而且它的触发频率也不是很稳定,所以React最终放弃了requestIdleCallback的使用。
在React中,官方实现了自己的任务调度库,这个库就叫做Scheduler。它可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,高级任务先执行,低优先级任务后执行。Scheduler存储在packages/scheduler文件夹中。
Reconciler协调层
在React15版本中,协调层和渲染层交替执行,即找到了差异就直接更新差异。
但在React16中,这种情况发生了变化,协调层和渲染层不再交替执行。协调层负责找出差异,在所有差异找出之后,统一交给渲染层进行DOM
更新。也就是说协调层的主要任务就是找出差异部分,并为差异打上标记。
Renderer渲染层
渲染层根据协调层为Fiber节点打的标记,同步执行对应的DOM操作。
既然比对的过程从递归变成了可以中断的循环,那么React是如何解决中断更新时DOM渲染不完全的问题呢?
答案是根本就不存在这个问题。因为在整个过程中,调度层和协调层的工作是在内存中完成的,是可以被打断的,而渲染层的工作被设定成不可被打断,会直接执行完渲染的任务,所以不存在DOM渲染不完全的问题。
Fiber
React 16之前的版本对比更新VirtualDom的过程时采用循环加递归实现的。这种方式有 一个问题,就是一旦任务开始进行就无法中断。如果应用中组件数量庞大,主线程被长期占用,直到整棵VirtualDom树对比更新完成之后主线程才能被释放,才能执行其他任务,这就导致一些用户交互、动画等任务无法立即得到执行,页面就会产生卡顿,非常的影响用户体验。
解决方案
- 利用浏览器空闲时间执行任务,拒绝长时间占用主线程。
- 放弃递只采用循环,因为循环可以被中断。
- 任务拆分,将任务拆分成小的任务。(这样的话任务重新执行所需的代价就会小很多)
对比操作
在Fiber方案中,为了实现任务的终止再继续,DOM对比算法被分成了两部分:
1、构建Fiber (可中断)
2、提交 Commit(不可中断)
DOM初始渲染: VirtualDom => Fiber => Fiber[] => DOM
DOM更新操作: newFiber vs oldFiber => Fiber[] => DOM
Fiber对象
Fiber其实就是JavaScript对象,是由VirtualDom对象演变而来的,在Fiber对象中有很多属性,如下是一些比较重要的属性:
type Fiber = {
/****************Dom实例相关***************/
// 标记不同的组件类型 workTag 即不同的节点类型对应的值 0-22之间的数值
tag: workTag,
// 组件类型 div span 组件构造函数
type: any,
// 实例对象,如类组件的实例,原生 DOM 实例,而function 组件没有实例,因此该属性为空
stateNode: any,
/************构建 Fiber 相关*************/
// 指向自己的父级 Fiber 对象
return: Fiber | null
// 指向自己的第一个子集 Fiber 对象
child: Fiber | null,
// 指向自己的下一个兄弟 Fiber 对象
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,
// 子树中第一个 side effect
firstEffect: Fiber | null
// 单链表用来快速查找下一个 side effect
nextEffect: Fiber | null
// 任务的过期时间
expirationTime: ExpirationTime,
// 当前组件及子组件处于何种渲染模式 详见 TypeOfNode
mode: TypeOfNode
}
TypeOfNode
:
export const TypeOfNode = number
// 0 同步渲染模式
export cosnt NoMode = 0b000
// 1 严格模式
export const StrictMode = 0b0001
// 10 异步渲染过渡模式
export const BlockingMode = 0b0010
// 100 异步渲染模式
export const ConcurrentMode = ob0100
// 1000 性能测试模式
export const ProfileMode = ob1000
双缓存技术
在React中,DOM的更新采用了双缓存技术,双缓存技术致力于更快速的DOM更新。
什么是双缓存?举个例子,使用canvas绘制动画时,在绘制每一帧前都会清除上一帧的画面,清除上一帧需要花费时间,如果当前贞画面计算量又比较大,又需要花费比较长的时间,这就导致上一帧清除到下一帧显示中间会有较长的间隙,就会出现白屏。
为了解决这个问题,可以在内存中绘制当前帧动画,绘制完毕之后直接用当前帧替换上一帧画面,这样的话在帧画面替换的过程中就会节约非常多的时间,就不会出现白屏问题。这种在内存中构建并直接替换的技术叫做双缓存。
React使用双缓存技术完成Fiber树的构建与替换,实现DOM对象的快速更新。
在React中最多会同时存在两棵Fiber树,当前在屏幕中显示的内容对应的Fiber树叫做current Fiber树,当发生更新时,React会在内存中重新构建一棵新的Fiber树,这棵正在构建的Fiber树叫做workInProgress Fiber树。在双缓存技术中,workInProgress Fiber树就是即将要显示在页面中的Fiber树,当这棵Fiber构建完成之后,React会直接替换掉current Fiber树达到快速更新DOM的目的,因为workInProgress Fiber树是在内存中构建的,所以它的构建速度是非常快的。
一旦workInProgress Fiber树在屏幕上呈现,它就会变成current Fiber树。
在current FIber节点对象中有一个alternate属性指向对应的workInProgress Fiber节点对象,在workInProgress Fiber节点中有一个alternate属性也指向current Fiber节点对象。
如图:
rootFiber表示的是组件的挂载点对应的Fiber对象,即id为root的div所对应的Fiber对象。
React在初始渲染时会先构建这个div所对应的Fiber对象,构建完成之后就将这个Fiber对象看作是current Fiber树,之后会在这个Fiber对象中添加一个alternate属性,属性值为current Fiber树的拷贝,将拷贝出的fiber树作为workInProgress Fiber树。当然在workInProgress Fiber树中也添加了alternate属性,属性值指向的是current Fiber树。
接下来构建子集fiber对象的工作就全部在workInProgress Fiber树中完成了。即app组件p节点的构建。
当所有fiber节点的构建完成之后,就由workInProgress Fiber树替换掉current Fiber树,这样就完成了Fiber节点的构建替换了。
替换完成之后,workInProgress Fiber树就变成了current Fiber树。Fiber节点对象中是存储了对应的DOM节点对象的,即DOM构建是在内存中完成的。当所有的Fiber对象构建完成之后,所有的DOM对象也就构建完成了,这时就可以用内存中的DOM对象替换页面中的DOM对象。
这就是React中使用到的双缓存技术,目的即是实现更快速的DOM更新。
关于fiberRoot和rootFiber
fiberRoot表示Fiber数据结构对象,是Fiber数据结构中的最外层对象。
rootFiber表示组件挂载点对应的Fiber对象,比如React应用中默认的组件挂载点就是id为root的div
fiberRoot包含rootFiber,在fiberRoot对象中有一个current属性,存储rootFiber
rootFiber指向fiberRoot,在rootFiber对象中有一个stateNode属性,指向fiberRoot
在React应用中fiberRoot只有一个,而rootFiber可以有多个,因为render方法是可以调用多次的,每次调用render方法时,render方法的第二个参数就是rootFiber。
fiberRoot会记录应用的更新信息,比如协调器在完成工作后,会将成果存储在fiberRoot中。
如图:
React是如何渲染到页面中的
要将React元素渲染到页面中,分为两个阶段:
- render阶段
- commit阶段
render阶段
render阶段就是协调层负责的阶段,在这个阶段中,要为每一个React元素构建对应的Fiber对象,在构建Fiber对象的过程中,还要为此Fiber对象创建对应的DOM对象,并且给Fiber对象添加effectTag属性,标注当前Fiber对象对应的DOM对象要进行的操作。
新构建的Fiber对象称之为workInProgress Fiber树,也可以理解为待提交的Fiber树,当render阶段结束之后,会被保存在fiberRoot对象中之后就会进入到commit阶段。
在commit阶段,会先获取到render阶段的工作成果,即获取到保存到fiberRoot中的workInProgress Fiber树,之后就是根据Fiber对象中的effectTag属性进行相应的DOM操作。
相关源码及调用关系如下
// 源码路径:src\react\packages\react-dom\src\client\ReactDOMLegacy.js
/**
* @description: 渲染入口
* @param {*} element 要渲染的ReactElement
* @param {*} container 渲染容器
* @param {*} callback 渲染完成之后执行的回调函数
* @return {*}
*/
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
// 检测 container 是否是符合要求的渲染容器
// 即检测 container 是否是真实的DOM对象
// 如果不符合要求就报错
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
);
}
}
// 渲染子树到container中
return legacyRenderSubtreeIntoContainer(
// 代表父组件,因为此处是初始渲染,是没有父组件的,传递null占位
null,
element,
container,
// 是否为服务器端渲染 true: 是服务端渲染,false不是
// 如果是服务端渲染则需要复用 container 内部的dom元素
false,
callback,
);
}
legacyRenderSubtreeIntoContainer方法的作用是将子树渲染到container容器中,初始化Fiber数据结构并创建fiberRoot和rootFiber对象。
相关源码如下:
// src\react\packages\react-dom\src\client\ReactDOMLegacy.js
/**
* @description: 将子树渲染到容器中 (初始化Fiber数据结构: 创建 fiberRoot 及rootFiber)
* @param {*} parentComponent 父组件 初始渲染传入了null
* @param {*} children render方法中的第一个参数,要渲染的 ReactElement
* @param {*} container 渲染容器
* @param {*} forceHydrate true 为服务端渲染 false为客户端渲染
* @param {*} callback 组件渲染完成之后的回调函数
* @return {*}
*/
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
if (__DEV__) {
topLevelUpdateWarnings(container);
warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
}
// 检测 container是否是已经初始化的容器
// react 在初始化渲染时会为最外层容器添加 _reactRootContainer 属性
// react 会根据次属性进行不同的渲染方式
// root 不存在表示初始渲染
// root 存在 表示更新
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
// 初始化渲染操作
if (!root) {
// 初始化根 Fiber 数据结构
// 为 container 容器添加 _reactRootContainer 属性
// 在 _reactRootContainer 对象中有一个属性叫做 _internalRoot
// _internalRoot 属性即为 FiberRoot 表示根节点 Fiber 数据结构
// legacyCreateRootFromDOMContainer
// createLegacyRoot
// new ReactDOMBlockingRoot -> this._internalRoot
// createRootImpl
// 其实就是在创建 fiberRoot以及rootFiber并为这两个对象添加一些默认属性
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 更新操作
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
// 返回实例对象
return getPublicRootInstance(fiberRoot);
}
legacyCreateRootFromDOMContainer中初始化了根Fiber对象的数据结构,为container容器添加了_reactRootContainer属性,相关代码如下:
// src\react\packages\react-dom\src\client\ReactDOMLegacy.js
/**
* @description: 判断是否为服务端渲染 如果不是则清空 container 容器中的节点
* @param {*} container dom对象
* @param {*} forceHydrate 是否是服务端渲染
* @return {*}
*/
function legacyCreateRootFromDOMContainer(
container: Container,
forceHydrate: boolean,
): RootType {
// 检测是否是服务端渲染
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// 如果不是
if (!shouldHydrate) {
let warned = false;
//
let rootSibling;
// 开启循环 删除 container 容器中的节点
while ((rootSibling = container.lastChild)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
console.error(
'render(): Target node has markup rendered by React, but there ' +
'are unrelated nodes as well. This is most commonly caused by ' +
'white-space inserted around server-rendered markup.',
);
}
}
// 删除子元素
container.removeChild(rootSibling);
/**
* 为什么要清除 container 中的元素?
* 有时需要在 container 中放置一些占位图或者 loading 图以提高首屏加载用户体验
* 就无可避免的要向 container 中添加html标记
* 在将 ReactElement 渲染到 container 之前,必然要先清空 container
* 因为占位图和 ReactElement 不能同时显示
*
* 所以在加入占位代码时,最好只有一个父级元素,可以减少内部代码的循环次数以提高性能
*/
}
}
if (__DEV__) {
if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
warnedAboutHydrateAPI = true;
console.warn(
'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
'will stop working in React v17. Replace the ReactDOM.render() call ' +
'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
);
}
}
// createLegacyRoot方法通过实例化ReactDOMBlockingRoot 类创建LegacyRoot
return createLegacyRoot(
container,
shouldHydrate
? {
hydrate: true,
}
: undefined,
);
}
createLegacyRoot代码如下:
// src\react\packages\react-dom\src\client\ReactDOMRoot.js
/**
* @description: 通过它可以创建 LegacyRoot 的Fiber数据结构
* @param {*} container dom对象
* @param {*} options
* @return {*}
*/
export function createLegacyRoot(
container: Container,
options?: RootOptions,
): RootType {
return new ReactDOMBlockingRoot(container, LegacyRoot, options);
}
ReactDOMBlockingRoot方法中只是初始化了container的_internalRoot属性值,即fiberRoot对象,并在fiberRoot对象的current属性指向rootFiber对象
// src\react\packages\react-dom\src\client\ReactDOMRoot.js
/**
* @description: 通过它可以创建 LegacyRoot 的Fiber数据结构
* @param {*}
* @return {*}
*/
function ReactDOMBlockingRoot(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
this._internalRoot = createRootImpl(container, tag, options);
}
在createRootImpl中调用了createContainer去生成一些对象,其中包含fiberRoot和rootFiber:
// src\react\packages\react-dom\src\client\ReactDOMRoot.js
function createRootImpl(
container: Container,
tag: RootTag,
options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
// 服务端渲染相关
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
// 此处生成 fiberRoot及rootFiber
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
markContainerAsRoot(root.current, container);
// 服务端渲染相关
if (hydrate && tag !== LegacyRoot) {
const doc =
container.nodeType === DOCUMENT_NODE
? container
: container.ownerDocument;
eagerlyTrapReplayableEvents(container, doc);
}
return root;
}
而fiberRoot和rootFiber是在createContainer中调用的createFiberRoot方法生成的:
// src\react\packages\react-reconciler\src\ReactFiberReconciler.js
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
// src\react\packages\react-reconciler\src\ReactFiberRoot.js
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// 创建根节点对应的 rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 为 rootFiber 添加 current 属性 值为 rootFiber
root.current = uninitializedFiber;
// 为 rootFiber 添加 stateNode 属性,值为 fiberRoot
uninitializedFiber.stateNode = root;
// 为 fiber 对象添加 updateQueue 属性,初始化 updateQueue 对象
// updateQueue 用于存放 Update 对象
// Update 对象用于记录组件状态的改变
initializeUpdateQueue(uninitializedFiber);
return root;
}
创建任务并存放于任务队列
在updateContainer方法(调用顺序为:render->legacyRenderSubtreeIntoContainer->updateContainer)中最核心的事情是创建一个任务对象,比如渲染DOM。在任务创建完成后会将任务放置在一个任务队列当中,之后就会等待浏览器的空闲时间,当有空闲时间时就会执行这个任务。相关源码如下:
// src\react\packages\react-reconciler\src\ReactFiberReconciler.js
/**
* @description: 计算任务的过期时间,再根据任务过期时间创建Update任务,通过任务的过期时间还可以计算出任务的优先级
* @param {*} element 要渲染的 ReactElement 对象
* @param {*} container Fiber Root 对象
* @param {*} parentComponent 父组件 初始渲染为 null
* @param {*} callback 渲染完成执行的函数
* @return {*}
*/
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): ExpirationTime {
// container 获取 rootFiber
// current currentTime suspenseConfig 是为了计算任务的过期时间
const current = container.current;
// 获取当前距离 react 应用初始化的时间
const currentTime = requestCurrentTimeForUpdate();
// 异步加载设置
const suspenseConfig = requestCurrentSuspenseConfig();
// 计算过期时间
// 为防止任务因为优先级原因一直被打断而未能被执行
// react 会设置一个过期时间,当时间到了过期时间的时候
// 如果任务还未执行的话,react将强制执行该任务
// 初始化渲染时,任务同步执行不涉及被打断问题
// 过期时间设置成了 1073741823 是固定的 这个表示当前任务为同步任务
const expirationTime = computeExpirationForFiber(
currentTime,
current,
suspenseConfig,
);
// 设置FiberRoot.context 首次执行返回一个 emptyContext是一个{}
const context = getContextForSubtree(parentComponent);
// 初始渲染时 Fiber Root 对象中的context属性值为null
// 所以会进入到if中
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// 创建一个待执行任务
const update = createUpdate(expirationTime, suspenseConfig);
// 将要更新的内容挂载到更新对象中的payload中
// 将要更新的组件存储至payload中,方便以后读取
update.payload = {element};
callback = callback === undefined ? null : callback;
// 判断callback是否存在
if (callback !== null) {
// 将callback挂载到update对象中
// 其实就是一层层传递 方便 ReactElement 元素渲染完成调用
// 回调函数执行完成后会被清除 可以在代码的后面加上return进行验证
update.callback = callback;
}
// 将update对象加入到当前Fiber的更新队列中(updateQueue)
// 待执行的任务都会被存储在 fiber.updateQueue.shared.pending中
enqueueUpdate(current, update);
// 调度和更新 current 对象
scheduleWork(current, expirationTime);
// 返回过期时间
return expirationTime;
}
将任务放在任务队列的相关源码如下:
/**
* @description: 将任务存放于任务队列中,创建单向链表结构存放 update next 用来串联update
* @param {*}
* @return {*}
*/
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// 获取当前 Fiber 的更新队列
const updateQueue = fiber.updateQueue;
// 如果更新队列不存在,就返回null
if (updateQueue === null) {
// 仅发生在 fiber已经被卸载
return;
}
// 获取待执行的 Update 任务
// 初始渲染时没有待执行的任务
const sharedQueue = updateQueue.shared;
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
// 将 Update 任务存储在 pending 属性中
sharedQueue.pending = update;
if (__DEV__) {
if (
currentlyProcessingQueue === sharedQueue &&
!didWarnUpdateInsideUpdate
) {
console.error(
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
didWarnUpdateInsideUpdate = true;
}
}
}
任务执行前的准备工作
在scheduleWork方法中(调用顺序为render->legacyRenderSubtreeIntoContainer->updateContainer->scheduleWork)所做的最核心的事情是判断当前任务是否是同步任务。如果是同步任务,则调用同步任务入口函数。 相关源码如下:
// scheduleWork方法其实就是
// src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js下的scheduleUpdateOnFiber方法
/**
* @description: 判断任务是否是同步任务,如果是同步任务则调用同步入口
* @param {*}
* @return {*}
*/
export function scheduleUpdateOnFiber(
fiber: Fiber,
expirationTime: ExpirationTime,
) {
/**
* fiber:初始化渲染时为 rootFiber,即 <div id="root"></div>对应的fiber对象
* expirationTime:任务过期时间 => 1073741823
*/
/**
* 判断是否是无限循环的 update 如果是就报错
* 在 componentWillUpdate 或者 componentDidUpdate 生命周期函数中重复调用
* setState 方法时,可能会发生这种情况,React限制了嵌套更新的数量(50)以防止无限循环
* 限制的嵌套更新数量为50,可通过 NESTED_UPDATE_LIMIT 全局变量获取
*/
checkForNestedUpdates();
// 开发环境中执行的
warnAboutRenderPhaseUpdatesInDEV(fiber);
// 更新子节点中的过期时间,返回 FiberRoot
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
// 开发环境下执行 忽略
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
// 判断是否有高优先级任务打断当前正在执行的任务
// 初始化渲染时不会执行因为初始化渲染时并没有更高优先级的任务
checkForInterruption(fiber, expirationTime);
// 报告调度更新,测试环境执行,忽略
recordScheduleUpdate();
// 获取当前调度任务的优先级 数值类型 90开始 数值越大优先级越高
// 97 是普通优先级任务
const priorityLevel = getCurrentPriorityLevel();
// 判断任务是否是同步任务 Sync 的值为: 1073741823
if (expirationTime === Sync) {
if (
// 检查是否处于非批量更新模式
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// 检查是否没有处于正在渲染的任务
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// 在根上注册待处理的交互,以免丢失跟踪的交互数据
// 初始渲染时内部条件判断不成立,内部代码没有得到执行
schedulePendingInteractions(root, expirationTime);
// 同步任务入口点
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
// 初始渲染不执行
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority ||
priorityLevel === ImmediatePriority)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
}
构建workInProgress Fiber树的rootFiber
因为初始渲染属于同步任务,所以其入口为performSyncWorkOnRoot函数(src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js)。
当调用这个方法后,就说明已经正式进入了render阶段,为每一个react元素构建workInProgress Fiber树中的Fiber对象。
相关源码如下:
// src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js
/**
* @description: 同步任务入口点,进入render阶段,构建 workInProgress Fiber 树
* @param {*} root currentFiber 树当中的 Fiber 对象
* @return {*}
*/
function performSyncWorkOnRoot(root) {
// ...
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
// 构建 workInProgress Fiber 树及rootFiber 对象
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
}
// ...
}
/**
* @description: 构建 workInProgress Fiber 树 及 rootFiber 对象
* @param {*} root
* @param {*} expirationTime
* @return {*}
*/
function prepareFreshStack(root, expirationTime) {
root.finishedWork = null;
root.finishedExpirationTime = NoWork;
const timeoutHandle = root.timeoutHandle;
// 初始化渲染不执行 timeoutHandle => -1 noTimeout => -1
if (timeoutHandle !== noTimeout) {
// The root previous suspended and scheduled a timeout to commit a fallback
// state. Now that we have additional work, cancel the timeout.
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
// 初始化渲染不执行 workInProgress 全局变量 初始化为null
if (workInProgress !== null) {
let interruptedWork = workInProgress.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
// 构建 workInProgress Fiber 树的 fiberRoot 对象
workInProgressRoot = root;
// 构建 workInProgress Fiber 树中的rootFiber
workInProgress = createWorkInProgress(root.current, null);
renderExpirationTime = expirationTime;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootLatestProcessedExpirationTime = Sync;
workInProgressRootLatestSuspenseTimeout = Sync;
workInProgressRootCanSuspendUsingConfig = null;
workInProgressRootNextUnprocessedUpdateTime = NoWork;
workInProgressRootHasPendingPing = false;
if (enableSchedulerTracing) {
spawnedWorkDuringRender = null;
}
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
}
}
其中构建workInProgress Fiber树中的rootFiber是通过createWorkInProgress创建的。
// src\react\packages\react-reconciler\src\ReactFiber.js
// 构建 workInProgress Fiber 树 中的 rootFiber
// 构建完成后会替换current fiber
// 初始渲染 pendingProps 为 null
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
// current: current Fiber 中的 rootFiber
// 获取currentFiber 对应的 workInProgress Fiber
let workInProgress = current.alternate;
// 如果 workInProgress 不存在
if (workInProgress === null) {
// 创建 fiber 对象
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
// 属性复用
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
if (__DEV__) {
// DEV-only fields
if (enableUserTimingAPI) {
workInProgress._debugID = current._debugID;
}
workInProgress._debugSource = current._debugSource;
workInProgress._debugOwner = current._debugOwner;
workInProgress._debugHookTypes = current._debugHookTypes;
}
// 使用 alternate 存储 current
workInProgress.alternate = current;
// 使用 alternate 存储 workInProgress
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// We already have an alternate.
// Reset the effect tag.
workInProgress.effectTag = NoEffect;
// The effect list is no longer valid.
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}
// 属性复用
workInProgress.childExpirationTime = current.childExpirationTime;
workInProgress.expirationTime = current.expirationTime;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
expirationTime: currentDependencies.expirationTime,
firstContext: currentDependencies.firstContext,
responders: currentDependencies.responders,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
if (__DEV__) {
workInProgress._debugNeedsRemount = current._debugNeedsRemount;
switch (workInProgress.tag) {
case IndeterminateComponent:
case FunctionComponent:
case SimpleMemoComponent:
workInProgress.type = resolveFunctionForHotReloading(current.type);
break;
case ClassComponent:
workInProgress.type = resolveClassForHotReloading(current.type);
break;
case ForwardRef:
workInProgress.type = resolveForwardRefForHotReloading(current.type);
break;
default:
break;
}
}
return workInProgress;
}
workLoopSync方法解析
rootFiber指的就是id为root的div所对应的Fiber对象,当rootFiber对象构建完成之后,就需要构建其子集Fiber对象。子集的Fiber对象就是render方法的第一个参数,即element。而element的子集Fiber对象是通过workLoopSync方法构建的。
在workLoopSync方法中,通过一个while循环不断创建子集的Fiber,直到创建结果为null,这样就构建了除rootFiber之外的所有子集的Fiber对象。
// src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js
/**
* @description: 构建除rootFiber之外的所有子集fiber对象
* @param {*}
* @return {*}
*/
function workLoopSync() {
// workInProgress是一个fiber对象
// 它的值不为 null 意味着该fiber对象上仍然有要更新的要执行
// while方法支撑render阶段,所有 fiber 节点的构建
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
workInProgress = performUnitOfWork(workInProgress);
}
}
performUnitOfWork解析
在workLoopSync方法中通过调用performUnitOfWork来构建除rootFiber之外的其余子级Fiber对象。在react16版本中采用循环的方式来模拟递归完成Fiber节点的构建,即beginWork是从父到子构建,而completeUnitOfWork是从子到父构建Fiber对象。
// src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js
/**
* @description: 通过while循环构建子集Fiber对象
* @param {*} unitOfWork
* @return {*}
*/
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
const current = unitOfWork.alternate;
startWorkTimer(unitOfWork);
setCurrentDebugFiberInDEV(unitOfWork);
let next;
// 初始渲染未执行
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
// beginWork 从父到子 构建Fiber
next = beginWork(current, unitOfWork, renderExpirationTime);
}
resetCurrentDebugFiberInDEV();
// 为旧的 props 属性赋值
// 此次更新后 pendingProps 变为 memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 从子到父 构建其余节点 Fiber 对象
next = completeUnitOfWork(unitOfWork);
}
ReactCurrentOwner.current = null;
return next;
}
其中调用的beginWork方法是从父到子级来构建Fiber对象。在beginWork方法中,根据父节点的tag类型来构建不同类型的子节点的Fiber对象,其中rootFiber的tag为3,所以会先调用类型3所对应的updateHostRoot方法。
/**
* @description: 从上到下构建 Fiber
* @param {*}
* @return {*}
*/
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
): Fiber | null {
const updateExpirationTime = workInProgress.expirationTime;
// 判断是否有旧的 Fiber 对象
// 初始化渲染时 只有 rootFiber 节点存在 current
// if (current !== null)
// ...
// NoWork 常量 值为0 清空过期时间
workInProgress.expirationTime = NoWork;
// 根据当前 Fiber 的类型决定如何构建起子级 Fiber 对象
// 文件位置: shared/ReactWorkTags.js
console.log(workInProgress.tag)
switch (workInProgress.tag) {
// 2
// 函数组件第一次渲染时使用
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderExpirationTime,
);
}
// 16
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
// 0
case FunctionComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 1
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
}
// 3
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
// 5
case HostComponent:
return updateHostComponent(current, workInProgress, renderExpirationTime);
// ...
}
}
而在updateHostRoot方法中,首先获取了workInProgress的更新队列,根据更新队列来更新rootFiber的子元素。
// src\react\packages\react-reconciler\src\ReactFiberBeginWork.js
/**
* @description: 更新 hostRoot
* @param {*} current
* @param {*} workInProgress
* @param {*} renderExpirationTime
* @return {*}
*/
function updateHostRoot(current, workInProgress, renderExpirationTime) {
pushHostRootContext(workInProgress);
// 获取更新队列
const updateQueue = workInProgress.updateQueue;
// 获取新的props对象
const nextProps = workInProgress.pendingProps;
// 获取上一次渲染使用的 state
const prevState = workInProgress.memoizedState;
// 获取上一次渲染使用的 children
const prevChildren = prevState !== null ? prevState.element : null;
// 浅复制更新队列
// workInProgress.updateQueue 浅拷贝 current.updateQueue
cloneUpdateQueue(current, workInProgress);
// 获取 updateQueue.payload 赋值给 workInProgress.memoizedState
// 要更新的内容就是element 就是 rootFiber 的子元素
processUpdateQueue(workInProgress, nextProps, null, renderExpirationTime);
// 获取element所在对象
const nextState = workInProgress.memoizedState;
// 从对象中获取 element
const nextChildren = nextState.element;
// 在计算 state 后如果前两个 Children 相同的情况
// 初始化时:
// prevChildren => null
// nextState => App
if (nextChildren === prevChildren) {
resetHydrationState();
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const root: FiberRoot = workInProgress.stateNode;
// 服务端渲染相关
if (root.hydrate && enterHydrationState(workInProgress)) {
let child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
workInProgress.child = child;
let node = child;
while (node) {
node.effectTag = (node.effectTag & ~Placement) | Hydrating;
node = node.sibling;
}
} else {
// 构建子集 fiber 对象
// 构建完成子集 Fiber 对象之后,会把 Fiber 对象挂载到 workInProgress.child 对象中
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
resetHydrationState();
}
return workInProgress.child;
}
构建单个子集Fiber对象的情况
在之前已经在updateQueue中获取到了已经要渲染的子集元素,即render方法的第一个参数,对应到代码中就是传入到reconcileChildren方法中的nextChildren对象。reconcileChildren方法的作用就是构建子元素所对应的Fiber对象。
而创建子集Fiber对象是通过mountChildFibers方法调用生成的。
// src\react\packages\react-reconciler\src\ReactFiberBeginWork.js
/**
* @description: 构建子级Fiber对象
* @param {*} current 旧的Fiber对象 如果不是初始渲染的话,需要进行新旧Fiber对象的对比
* @param {*} workInProgress 新Fiber对象
* @param {*} nextChildren 子级 VDOM对象
* @param {*} renderExpirationTime 过期时间 初始渲染时为整形最大值,代表同步任务
* @return {*}
*/
function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
/**
* 为什么要传递 current ?
* 如果不是初始渲染,则要进行新旧DOM对比
* 初始渲染时则用不到
*/
// 如果为null 代表初始化渲染
if (current === null) {
// mountChildFibers真正构建了nextChildren所对应的fiber对象
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}
而mountChildFibers方法实际上就是ChildReconciler方法。在ChildReconciler中返回的就是一个reconcileChildFibers方法,所以实际上最终调用的是reconcileChildFibers方法。
在reconcileChildFibers方法中,首先判断了子级vdom是否为占位组件。如果是占位组件,则直接使用占位组件的子元素作为newChild,否则直接使用子级vdom。
其次检查newChild是否为对象类型。因为如果是对象类型的话,说明只有一个子级节点,如果是一个数组的话,说明有多个子级节点。
因为初始化渲染时rootFiber下的子集是App组件对应的Fiber对象,是一个object类型,所以会调用reconcileSingleElement方法来生成所对应的Fiber对象,至此构建单个子集Fiber对象过程结束。
// src\react\packages\react-reconciler\src\ReactChildFiber.js
/**
* @description: 构建子集Fiber对象
* @param {*} returnFiber 父Fiber对象
* @param {*} currentFirstChild 旧的第一个子 Fiber 初始化渲染时为null
* @param {*} newChild 新的vdom对象
* @param {*} expirationTime 过期时间 如果为整形最大值则代表同步任务
* @return {*}
*/
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
// 这是入口方法 根据 newChild 类型进行对应处理
// 判断新的子 vdom 是否为占位组件 比如 <></>
// 如果是占位组件则为true
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
// 如果newChild 为占位符,使用占位符组成的子元素作为 newChild
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 检测 newChild是否为对象类型
// 如果是对象说明子元素只有一个
// 如果是数组的话说明子元素有多个
const isObject = typeof newChild === 'object' && newChild !== null;
// newChild是单个对象时
if (isObject) {
// 匹配子元素的类型
switch (newChild.$$typeof) {
// 子元素为react元素的话
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
// 如果为 portal类型
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
}
}
// ...
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
悄悄说一句,这只是初稿~后面会优化。