本章节我们主要讲解Fiber Reconciler
协调流程,即FiberTree
的具体创建过程。
回到之前的renderRootXXX
方法:
js
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
下面的讲解我们将renderRootSync
同步渲染模式为例,因为常规的情况下react应用都是同步渲染模式创建的FiberTree
,并且并发渲染模式和同步渲染模式实际差异并不大,后续我们会讲解它们具体的差异,下面我们开始学习具体的创建过程。
1,renderRootSync
查看renderRootSync
源码:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
# 同步渲染模式
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
// 派发器:包含了所有hook方法
const prevDispatcher = pushDispatcher();
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
workInProgressTransitions = getTransitionsForLanes(root, lanes);
# 根据初始的【或者叫上一次的】hostFiber对象内容,来新建一个本次工作中的hostFiber,即WorkInProgress【非克隆】
// 这个函数调用之后,初始的root.current即hostFiber对象,它的alternate属性就有了内容,
// wrokInProgress就是工作中的hostFiber,是计算中的组件树根节点
prepareFreshStack(root, lanes);
}
# 开始执行工作循环
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 重置上下文依赖
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// 重置work,表示没有在进行中的工作内容
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
# 返回最后的渲染状态
return workInProgressRootExitStatus;
}
renderRootSync
方法的核心是一个do while
循环体,并且它的循环条件永远为真,代表为无限循环,只有workLoopSync
方法执行完成后,才能触发break
关键字,退出循环。
renderRootSync
方法每次执行时,都会在workLoop
工作循环开始之前,调用一个prepareFreshStack
方法,这个方法的作用是根据上一次的【旧的】HostFiber
对象内容【FiberTree
的根节点】,创建一个新的HostFiber
对象,作为本次创建FiberTree
的根节点。
也就是说每次创建具体FiberTree
之前,都会首先创建【或者说确定】用于本次FiberTree
的根节点HostFiber
。
prepareFreshStack
查看prepareFreshStack
方法:
js
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
// 重置当前的应用根节点的Fiber树
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
# 使用旧的hostFiber对象,来创建一个本次工作的hostFiber,即WorkInProgress
const rootWorkInProgress = createWorkInProgress(root.current, null);
# 并且更新全局变量workInProgress的值为 本次参与计算的hostFiber
workInProgress = rootWorkInProgress;
// 设置子节点的更新lanes值: 为本次更新的lanes
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
首先重置了root应用根节点上的Fiber
树和更新lanes
,然后调用createWorkInProgress
方法创建参与本次更新的HostFiber
,然后将创建结果赋值给全局变量workInProgress
,然后下面还初始化了一些其他的全局变量,都是为了本次的更新做准备工作。
注意:
workInProgress
是一个全局变量,代表当前正在工作中的Fiber
节点【动态更新的】,它并非固定指某一个Fiber
节点,只是在当前函数中,它的值被赋值为新建的HostFiber
,代表本次参与计算的FiberTree
根节点。
下面我们深入看一下createWorkInProgress
方法:
js
// packages\react-reconciler\src\ReactFiber.new.js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
# 首次应用加载
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
# 应用更新时
workInProgress.pendingProps = pendingProps;
// Needed because Blocks store data on type.
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
# 复用上一次HostFiber的内容
// Reset all effects except static ones.
// Static effects are not specific to a render.
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
...
return workInProgress;
}
createWorkInProgress
方法看着代码很多,实际上它的逻辑很简单,在讲解之前,再次贴出current
与workInProgress
的关系图:
首先从通过current.alternate
属性中取出对应的workInProgress
,然后下面就是一个if else
的条件语句:
js
let workInProgress = current.alternate;
if (workInProgress === null) {
// 应用首次加载,此时workInProgress一定时null,需要调用createFiber来新建参与计算的`HostFiber`
workInProgress = createFiber();
} else {
// 应用更新,无需新建workInProgress,复用current的相关内容即可
}
- 在react应用首次加载的时候,
current.alternate
一定是null
,所以需要调用createFiber
来新建参与计算的HostFiber
。 - 如果是应用更新阶段,这里的
current.alternate
指向就是上一次的【旧的】HostFiber
。
无论是新建的,还是取出旧的workInProgress
,它的后续内容都是复用current
的内容,所以说createWorkInProgress
方法它的作用就是确定本次参与计算的workInProgress
,在当前的内容中就是确定本次创建FiberTree
的根节点HostFiber
。
回到renderRootSync
方法,在讲解完prepareFreshStack
方法逻辑后,下面的内容就比较简单了。
剩下的内容主要就是一个do while
循环,最后返回一个渲染状态。并且我们可以发现do while
的判断条件永远为真,所以这是一个无限循环,只有workLoopSync
方法执行完成,才会触发break
关键字,跳出循环。
注意:
renderRootConcurrent
并发渲染模式,在这里也是一样的do while
循环逻辑,它们的差异在下面。
workLoopSync
继续查看workLoopSync
方法:
js
# 同步渲染模式工作循环
function workLoopSync() {
// while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress === null
while (workInProgress !== null) {
// 执行装置函数
// current是上一次的Fiber树,旧的; workInProgress是正在计算中的Fiber树,新的
performUnitOfWork(workInProgress);
}
}
workLoopSync
方法内只有一个while
循环,它的判断条件是workInProgress
不为null
,表示还有进行中的工作,则需要继续调用performUnitOfWork
方法,创建未完成的FiberTree
。所以同步渲染模式,一旦开始这个Fiber Reconciler
协调流程,就会进入一直循环的逻辑,直到创建出完整的FiberTree
,这个过程无法中断。
下面我们来看看并发渲染模式的方法。
workLoopConcurrent
查看workLoopConcurrent
方法:
js
# 并发渲染模式 工作循环
function workLoopConcurrent() {
// workInProgress === null 代表FiberTree的构建工作结束
// shouldYield可中断构建
while (workInProgress !== null && !shouldYield()) {
// 这个方法会创建下一个fiberNode赋值给 workInProgress,并将已创建的FiberNode链接起来构成FiberTree
performUnitOfWork(workInProgress);
}
}
可以看见和同步渲染模式基本一样,唯一的区别就是while
循环的多了一个判断条件:
scss
!shouldYield()
这个方法就是我们在第二章节遇见过shouldYieldToHost
,它的作用就是判断:当前程序运行时间是否小于帧间隔时间frameInterval
【默认5ms
】。
- 如果小于:则返回
false
,代表还有剩余可执行时间。取反后为true
,表示可以继续创建FiberTree
的工作循环。 - 如果大于:则返回
true
,代表没有剩余可执行时间。取反后为false
,则while
循环的条件将不再满足,会暂停创建FiberTree
的工作,结束本次宏任务,剩下的工作会留待下一次宏任务再处理。
所以并发渲染模式和同步渲染模式的主要区别 就是在这里:它可以中断FiberTree
的创建过程,而同步渲染模式是无法中断这个过程的,它只能从开始到创建完成。
并且在这里我们也可以看出,它们的while
循环里面都是执行了一个相同的performUnitOfWork
方法,所以后面的逻辑也就没有什么模式的区分了,都是执行的一样的内容。
在继续讲解performUnitOfWork
方法内容之前,这里要补充说明一下暂停的工作是如何恢复执行的?
暂停工作与恢复执行
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
// 并发渲染模式
function renderRootConcurrent() {
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// 检查tree工作是否完成
if (workInProgress !== null) {
// Still work remaining.
return RootInProgress;
}
}
当workLoopConcurrent
并发渲染的工作被中断后,就会退出do while
循环,然后就会检查当前的workInProgress
是否为null
,很明显被暂停的情况,workInProgress
是一定有值的,它的值为下一个即将处理的Fiber
节点。
此时workInProgress
不为null
,就会返回RootInProgress
的渲染状态,表示还有剩下的工作。
这里return
之后,就会回到performConcurrentWorkOnRoot
方法之中。
js
function performConcurrentWorkOnRoot(root, didTimeout) {
// 返回未完成的渲染状态
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
// 根据渲染状态 继续执行逻辑
if (exitStatus !== RootInProgress) {
...
}
// 渲染未完成的情况:
if (root.callbackNode === originalCallbackNode) {
// 渲染暂停的情况:继续返回performConcurrentWorkOnRoot,下一次宏任务继续处理
return performConcurrentWorkOnRoot.bind(null, root);
}
}
此时返回未完成的渲染状态,就不满足下面的条件判断,直接来后最后判断当前的回调任务是否还是原来的任务:
js
root.callbackNode === originalCallbackNode
很明显这里还是相同的回调任务,对应的就是渲染暂停的情况,这里就会继续返回这个performConcurrentWorkOnRoot
函数。
这里return
就会回到workLoop
方法之中:
js
function workLoop() {
...
while (currentTask !== null) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost()) ) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
...
const continuationCallback = callback(didUserCallbackTimeout);
// performConcurrentWorkOnRoot函数
if (typeof continuationCallback === 'function') {
// 说明任务还未完成,将任务继续设置未当前任务的callback,等待下次继续执行
// 这里没有删除这个任务,则下次取出的第一个任务,还是这个任务,
currentTask.callback = continuationCallback;
}
}
if (currentTask !== null) {
// 还有工作,则会生成一个新的宏任务,在下次的宏任务中继续执行剩下的任务
return true;
else {
...
}
}
在workLoop
方法之中,就会将返回的内容再次赋值给当前任务currentTask
的callback
属性,表示还有未完成的工作。workLoop
方法中的while
循环就会被中断,原因同样是shouldYieldToHost
方法。
在workLoop
方法最后就会判断当前任务currentTask
是否为null
,很明显被中断的情况,currentTask
是有值的,所以就会返回true
,这里会一直向上return
:
ini
=> flushWork => performWorkUntilDeadline
最后会将结果返回performWorkUntilDeadline
方法之中:
js
const performWorkUntilDeadline = () => {
...
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
// 此时hasMoreWork为true
if (hasMoreWork) {
// 如果还有任务,则又触发MessageChannel事件,生成新的宏任务,即在下一个消息事件继续执行任务
schedulePerformWorkUntilDeadline();
} else {
...
}
}
此时hasMoreWork
为true
,就会调用schedulePerformWorkUntilDeadline
方法,生成一个新的宏任务,在下个宏任务中继续执行剩下的任务。
关于上面这部分调度的逻辑可以查看《React18.2x源码解析(二)scheduler调度程序》。
恢复执行的原理也很简单:在下次的宏任务中继续创建FiberTree
的时候,因为workInProgress
是一个全局变量,一直保存在内存之中,并且它内容为下一个即将处理的Fiber
节点,所以下次再回到这里时,就会自动从这个Fiber
节点开始继续执行剩下的创建工作。
js
function workLoopConcurrent() {
// workInProgress === null 代表FiberTree的构建工作结束
// shouldYield可中断构建
while (workInProgress !== null && !shouldYield()) {
// 这个方法会创建下一个fiberNode赋值给 workInProgress,并将已创建的FiberNode链接起来构成FiberTree
performUnitOfWork(workInProgress);
}
}
注意: 这里的下次并不一定是指的下一个宏任务,因为有可能会遇到更高优先级的任务排在前面执行,比如浏览器执行渲染相关的宏任务,这就是react框架使用调度器与时间切片功能的意义所在。
2,performUnitOfWork
案例准备
在讲解具体的FiberTree
创建过程之前,我们准备一个基础的案例,配合案例可以帮助我们更好的理解创建的流程。
js
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
js
// App.js
import MyFun from './views/MyFun';
import MyClass from './views/MyClass';
export default function App() {
console.log('App组件运行了')
return (
<div className="App">
<div>react源码调试</div>
<MyFun name='MyFun'></MyFun>
<MyClass name='MyClass'></MyClass>
</div>
);
}
js
// MyFun.js
import React from 'react'
import { useState } from 'react'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
return (
<div className='MyFun'>
<div>MyFun组件</div>
<div>state: {count}</div>
<div>name: {props.name}</div>
</div>
)
}
js
// MyClass.js
import React, { Component } from 'react';
export default class MyClass extends Component {
constructor(props) {
super(props)
console.log('MyClass组件运行了')
this.state = {
count: 1
}
}
componentDidMount() {
console.log('MyClass组件mount完成')
}
render() {
return (
<div className='MyClass'>
<div>MyClass组件</div>
<div>state: {this.state.count}</div>
<div>name: {this.props.name}</div>
</div>
);
}
}
Fiber树结构
下图是案例对应的FiberTree
树结构:
上图是根据案例绘制的对应的FiberTree
结构,方便大家理解,后面阅读beginWrok
和completeWork
的执行逻辑时可以根据此图印证。每一个单元格都是一个Fiber
对象【FiberNode
实例】,Fiber
对象的child
属性指向它的第一个子节点【FirstChild
】,Fiber
对象的return
属性指向它的父级节点。
这里有一个注意的点:
js
// 非纯静态节点
<div>state: {count}</div>
即节点的内容中包含了动态的数据内容{count}
,react在创建FiberTree
时,会进行分离,创建成独立的Fiber
节点,也就是如上图所示一样,它还会继续分成两个子节点。
所以图中的name: {props.name}
以及后面class组件中的对应内容都是一样处理逻辑,它们也还会有自己的子节点,图中没有展示完全是因为会增加图片的复杂度【影响阅读】,所以没有补全这几个内容的后续节点,只补充了state: {count}
这个节点的子节点内容,这一点细节大家只要了解即可,同时在这里我们也可以知道DOM结构和FiberTree
并不是一一对应的。
下面正式进入FiberTree
的创建过程。
performUnitOfWork
查看performUnitOfWork
方法:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate; // 上一次,旧的节点
// 开始循环构建
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
// 开始工作
next = beginWork(current, unitOfWork, subtreeRenderLanes);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
/**
* 注意:当遍历到叶子元素,即没有子FiberNode时,performUnitOfWork会进入归的阶段。
* 这个阶段会调用completeUnitOfWork处理FiberNode,当某个FiberNode阶段执行completeUnitOfWork方法后,
* 如果存在其他的兄弟节点【FiberNode.sibling !== null】,会进入兄弟节点的归阶段,
* 如果不存在其他兄弟节点,则进入父节点的归阶段,递阶段和归阶段会交替进行直到HostFiber的归阶段
* 至此,render工作结束,Fiber树创建完成。
*
*/
if (next === null) {
// 这个阶段会调用completeUnitOfWork处理FiberNode,
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
首先从workInProgress.alternate
属性中取出current
对象,对于这个current对象,你可以理解为这个节点上一次对应的内容【因为在参与计算时,workInProgress
一定是新的节点,current
则变成了旧的节点】,这里在调用beginWork
时,同时传入了current
和workInProgress
,它的作用就是方便后续区分这个节点是更新还是初始加载,以及为更重要的diff
过程做准备。
关于
current
和workInProgress
,对比Vue源码,就是patch过程中的patch( oldVNode,VNode )
。
3,beginWork
继续查看beginWork
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// update更新流程
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
...
} else {
didReceiveUpdate = false;
// mount加载流程
if (getIsHydrating() && isForkedChild(workInProgress)) {
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
workInProgress.lanes = NoLanes;
# 根据不同的tag,进入不同的处理逻辑:
switch (workInProgress.tag) {
// 函数组件 mount
case IndeterminateComponent: {
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
// 函数组件 update
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,
renderLanes,
);
}
// 类组件分支
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
// 处理class App组件【此时App组件对应的FiberNode已经创建】
return updateClassComponent(
current,
workInProgress, // 存储的App组件对应的FiberNode
Component, // FiberNode.type
resolvedProps,
renderLanes,
);
}
// 根节点的处理【第一次都会走这里】
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent: // 代表原生Element元素,div,span
return updateHostComponent(current, workInProgress, renderLanes);
case HostText: // 文本类型
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
// 处理ForwardRef组件 11
case ForwardRef: {
const type = workInProgress.type;
...
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
// memo组件处理 14
// 简单MemoComponent在首次创建时也会走这里,更新时会走下面updateSimpleMemoComponent
// 因为它会在进入MemoComponent后,进入另外一个分支,走updateSimpleMemoComponent
case MemoComponent: {
const type = workInProgress.type;
...
}
// 简单memo组件:15
case SimpleMemoComponent: {
...
}
case IncompleteClassComponent: {
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
// 省略
...
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
beginWork
方法的内容比较多,下面我们按顺序进行解析:
首先是一个current
的判断:
csharp
if (current !== null) {
// 更新流程
} else {
// 加载流程
}
根据前面的performUnitOfWork
方法可以知道:current
取之于workInProgress.alternate
属性,代表着旧的节点。
js
function performUnitOfWork() {
const current = workInProgress.alternate;
beginWork(current, workInProgress, subtreeRenderLanes);
}
这里我们要注意: 第一次进入beginWork
工作的节点一定是HostFiber
,也就是FiberTree
的根节点。
js
// packages\react-reconciler\src\ReactWorkTags.js
export const HostRoot = 3; // 根节点
tag
为3
代表着此节点为HostFiber
。
当前的workInProgress
为HostFiber
,则current
一定是有值的,因为它的current在最开始的createRoot
方法中就已经创建了,而workInProgress
是在进入workLoopSync
工作循环之前创建的,所以它在beginWork
方法中永远都不会为null
,它会进入更新的流程。
然后beginWork
方法后续的内容就是根据workInProgress.tag
的值,进入不同的分支处理逻辑。
js
switch (workInProgress.tag) {
case FunctionComponent: {},
case ClassComponent: {},
case HostRoot: {},
...
}
注意这个switch case
结构【根据tag
值执行不同的组件逻辑】,它在react源码中应用非常广泛,无论是在render
阶段还是commit
阶段中都有它的身影,唯一的区别是执行的细节不同而已。
当前为HostFiber
则会进入HostRoot
分支,进行根节点的处理。
js
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
updateHostRoot
继续查看updateHostRoot
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
function updateHostRoot(current, workInProgress, renderLanes) {
const nextProps = workInProgress.pendingProps;
// 之前的state
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// 处理后的state
const nextState: RootState = workInProgress.memoizedState;
const nextChildren = nextState.element; // 存储的组件的内容
...
// 如果新旧children相同,则会进入优化的逻辑,跳过本次reconcile协调流程,复用原来的节点内容
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
# 不相同,则进入diff过程,创建新的子节点内容
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}
// 返回新建的子节点
return workInProgress.child;
}
进入updateHostRoot
方法后,主要是根据新旧children
是否相同来进入不同的逻辑处理:
bailoutOnAlreadyFinishedWork
:代表新旧children
相同,满足优化策略条件,可以进入react内部的优化逻辑,如果满足内部优化逻辑,则可以跳过本节点的reconcile
协调流程,复用原来的节点内容。reconcileChildren
:不满足优化策略,代表子节点需要更新,进入本节点的reconcile
协调流程,创建新的子节点内容。
最后返回新的子节点内容。
reconcileChildren
这里的HostFiber
节点,会进入reconcileChildren
方法:
js
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
# 加载流程
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
# 更新流程, 更新当前节点的child内容
workInProgress.child = reconcileChildFibers(
workInProgress, // 父节点
current.child, // 旧的child, null
nextChildren, // 新的child
renderLanes, // 本次更新的lanes
);
}
}
根据前面已经知道,HostFiber
对应的current
一定是有值的【FiberTree
的根节点一个特殊的节点,它的加载和普通的节点有所不同】,所以这里它会进入reconcileChildFibers
分支,开始更新HostFiber
的child
内容【也就是App根组件】。
注意: 这里的mountChildFibers
和reconcileChildFibers
其实都是ChildReconciler
方法调用的返回值,代表着它们其实是同一个方法。它们的区别在于ChildReconciler
调用时传入了一个不同的状态,即shouldTrackSideEffects
参数,这个参数表示是否追踪副作用。
js
// packages\react-reconciler\src\ReactChildFiber.new.js
// 更新子元素
export const reconcileChildFibers = ChildReconciler(true);
// 创建子元素
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
...
function reconcileChildFibers() {}
return reconcileChildFibers;
}
reconcileChildFibers
继续查看reconcileChildFibers
方法:
js
// packages\react-reconciler\src\ReactChildFiber.new.js
function reconcileChildFibers(
returnFiber: Fiber, // 父节点
currentFirstChild: Fiber | null, // 子节点
newChild: any, // 新的child内容
lanes: Lanes,
): Fiber | null {
// 根据新的child类型,进行不同的处理
if (typeof newChild === 'object' && newChild !== null) {
// 1,单个子节点处理
switch (newChild.$$typeof) {
# 最常见的react-element元素类型:react中类组件,函数组件,普通dom组件都属于此类型
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
... // 省略
}
// 2,数组节点的处理,即多个子节点,比如div.App下面有三个子节点
// 循环创建多个子节点,最后返回第一个子节点,firstChild
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 3,处理文本子节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes,
),
);
}
// newChild为null时,表示没有子节点了,会返回一个null,由此一组beginWork工作执行完成,
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
reconcileChildFibers
方法就是根据新的子节点类型,来创建新的child
内容,HostFiber
的子节点为App根组件,如图所示:
进入第一个分支,开始单个子节点的处理过程:
js
return placeSingleChild(reconcileSingleElement())
它的处理是两个函数的调用,返回最后的调用结果,这里我们先看里面的函数调用。
reconcileSingleElement
查看reconcileSingleElement
方法:
js
// packages\react-reconciler\src\ReactChildFiber.new.js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
# 更新处理 【单节点diff】
while (child !== null) {
if (child.key === key) {
const elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
// 更新为兄弟节点:再次处理更新逻辑
child = child.sibling;
}
# 创建处理
if (element.type === REACT_FRAGMENT_TYPE) { // 片段类型
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
created.return = returnFiber;
return created;
} else {
// 其他类型
// 根据ReactElement元素对象,创建FiberNode节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
// 设置App组件的FiberNode.return 指向hostFiberRoot根节点
created.return = returnFiber;
return created;
}
}
这里的child
是前面传递的current.child
,这是首次加载,所以current
【hostFiber
】肯定没有子节点内容,所以为null
,直接进入下面的创建过程:
App组件为react-element
元素类型,不属于fragment
类型,进入else
分支的创建逻辑。
下面的内容比较重要,正式进入组件节点的创建过程。
createFiberFromElement
查看createFiberFromElement
方法:
js
// packages\react-reconciler\src\ReactFiber.new.js
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
const type = element.type; // 存储原始的组件内容,比如fun,class
const key = element.key;
const pendingProps = element.props; // 等待处理的,最新的props
// 创建Fiber节点: 根据元素的type ,针对组件来说:type值一般为class类 或者Fun函数
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
# 返回新建的Fiber节点
return fiber;
}
createFiberFromElement
方法很重要,react所有reactElement
元素都是通过这个方法来创建对应的Fiber
节点。比如常见的class组件,fun组件,原生dom元素等,我们在项目中定义的组件和元素最终都会通过createElement
方法转换为一个reactElement
元素对象。
js
// react.createElement(<App />)
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ƒ App() // class组件就是 type: class App{}
}
// react.createElement(<div />)
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: 'div'
}
所以这里createFiberFromElement
方法就是通过这个reactElement
对象来创建对应的Fiber
节点,最后返回新建的Fiber
节点。
createFiberFromTypeAndProps
继续查看createFiberFromTypeAndProps
方法:
js
// packages\react-reconciler\src\ReactFiber.new.js
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: null | string,
pendingProps: any,
owner: null | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
# 设置初始的fiber节点tag
let fiberTag = IndeterminateComponent;
let resolvedType = type;
# 根据元素的type进行不同的逻辑处理,class也是函数
if (typeof type === 'function') {
// 针对类组件的特殊处理
if (shouldConstruct(type)) {
// 根据type 设置fiberTag 为class组件
fiberTag = ClassComponent;
} else {
# 普通函数组件,没有更新tag【重要,伏笔】
}
} else if (typeof type === 'string') {
fiberTag = HostComponent; // 原生dom的处理
} else {
// 其他type类型的处理
...
}
# 创建Fiber
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
return fiber;
}
createFiberFromTypeAndProps
方法看着代码虽然多,但实际上主要是两部分逻辑的处理:
- 首先定义初始的
fiberTag
值,然后根据元素对象的type
类型来更新fiberTag
值。
js
let fiberTag = IndeterminateComponent;
这里的IndeterminateComponent
表示待定的组件类型,也就是没有确定的组件类型。
下面列出react源码中常见的tag
值对应的组件类型:
js
// packages\react-reconciler\src\ReactWorkTags.js
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 待定的类型
export const HostRoot = 3; // 根节点类型
export const HostPortal = 4;
export const HostComponent = 5; // 指的是原生dom 标签元素
export const HostText = 6; // 文本类型
export const Fragment = 7; // 片段
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
...
然后根据type
类型更新fiberTag
,这里我们主要关注类组件,函数组件以及普通的dom元素处理即可。
js
if (typeof type === 'function') {
// 针对类组件的特殊处理
if (shouldConstruct(type)) {
// 根据type 设置fiberTag 为class组件
fiberTag = ClassComponent;
} else {
# 普通函数组件,没有更新tag【重要,伏笔】
}
} else if (typeof type === 'string') {
fiberTag = HostComponent; // 原生dom的处理
} else {
...
}
这里要注意,class类本质是构造函数,所以它也是function
类型:
js
class App{}
typeof App // 'function'
所以类组件和函数组件都会进入function
进行处理,然后这里又通过shouldConstruct
方法对这两个组件进行了区分:
js
function shouldConstruct(Component: Function) {
const prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
在react应用中我们定义的class
组件,都是通过继承react的内部组件Component
来定义的,所以通过它原型上的isReactComponent
属性可以来区分当前组件是不是类组件,返回true
则是类组件,返回false
则是函数组件。
关于
Component
内置组件原理可以查看《React18.2x源码解析:React常用API原理》
如果是类组件就会更新fiberTag
为ClassComponent
的值。
注意: 如果是函数组件,这里并没有更新fiberTag
,所以函数组件的fiberTag
还是为待定的IndeterminateComponent
的值。
函数组件在创建
Fiber
节点时,tag
值还是待定的组件类型。该节点会在首次执行beginWork
工作时,在mountIndeterminateComponent
方法中更新tag
值为正确的函数类型。
最后是针对普通的dom元素的处理,更新为HostComponent
的值。
js
else if (typeof type === 'string') {
fiberTag = HostComponent; // 原生dom的处理
}
- 然后根据前面准备的信息,调用
createFiber
创建新的Fiber
节点,最后返回新创建的Fiber
。
关于
createFiber
方法创建Fiber
节点的过程可查看《React18.2x源码解析(一)react应用加载》。
最后在创建完成后,返回新建的Fiber
对象。
查看当前创建完成的App
组件对应的Fiber
对象:
在createFiberFromTypeAndProps
方法执行完成后,就会return
回到createFiberFromElement
方法中。
然后继续return
回到reconcileSingleElement
方法:
js
function reconcileSingleElement() {
...
// 根据ReactElement元素对象,创建FiberNode节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
# 设置Fiber对象return属性 指向它的父级节点
created.return = returnFiber; // 当前则为hostFiber
return created;
}
在根据ReactElement
元素对象,创建完成对应的Fiber
节点之后,下一步是针对ref
的处理,因为我们这里的案例并没有使用ref
,所以ref
还是null
,然后设置Fiber
节点的return
属性,这个return
属性指向的是它的父级节点【当前新建的Fiber
属于App组件,所以它的父级节点就是HostFiber
】。
最后返回创建完成的Fiber
节点,继续向上return
,则会来到placeSingleChild
方法中:
js
// place插入操作
function placeSingleChild(newFiber) {
# 新建的Fiber节点alternate都是null
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
这个方法只有一个作用,就是给新建的Fiber
节点设置flags
属性值为Placement
【插入的标识,用于commit
阶段的逻辑执行】。
注意:因为之前
HostFiber
是走的reconcileChildFibers
方法,这里的shouldTrackSideEffects
为true
,可以回到前面查看关于这部分内容的解析。所以当前App组件对应的Fiber
节点满足这里的判断条件,它的flags
属性就会被打上Placement
插入的标记。但是其他节点都是走的mountChildFibers
,所以都无法进入判断,最终react应用首次创建FiberTree
时,只有App根组件这一个Fiber
节点被打上了Placement
【插入标记】。它的作用就是在commit
阶段,只需要执行一次插入操作,即可将一个完整的离屏DOM树立即渲染到页面【首屏渲染的优化】。
后面的内容就是一直向上return
,返回新建Fiber
节点,直到回到performUnitOfWork
方法中:
js
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate; // 上一次
// 开始循环构建
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
# 此时next的值就是 新建的App组件对应的Fiber对象
next = beginWork(current, unitOfWork, subtreeRenderLanes);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 这个阶段会调用completeUnitOfWork处理FiberNode,
completeUnitOfWork(unitOfWork);
} else {
# next有值,开启下一个节点的beginWork工作
workInProgress = next;
}
}
到此时,HostFiber
节点的beginWork
的工作就已完成,next
就等于新建的Fiber
节点【App组件对应的】。
然后进行判断,此时next
有值,则更新workInProgress
,将它设置为新的Fiber
节点。
js
workInProgress = next;
workInProgress
之前是hostFiber
,此时就已经变成了App组件对应的Fiber
节点。
performUnitOfWork
方法执行完成,再次回到workLoopSync
方法中,此时就代表一个Fiber
节点的beginWork
工作执行完成。
js
function workLoopSync() {
// while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress===null
while (workInProgress !== null) {
// 执行装置
performUnitOfWork(workInProgress);
}
}
此时workInProgress
依然有效的Fiber
节点,所以满足while
循环的条件,就会再次进入performUnitOfWork
方法,开启下一个的 Fiber
节点【App组件】的beginWork
工作。
如果当前是并发渲染模式
workLoopConcurrent
,react就有机会可以中断这个循环过程,暂停FiberTree
的创建,释放主线程,让浏览器执行更高优先级的任务,剩下的FiberTree
创建任务等待下一次的宏任务再执行。
到此为止,一个Fiber
节点的beginWork
工作就此结束。
多节点的创建
下面我们再来了解一下多子节点的创建:
比如案例中的【div.app】Fiber
节点,它有三个子节点:
css
div 源码调试
myFun
myClass
当传入的newChild
为数组时,就会进入多子节点的创建过程:
js
// packages\react-reconciler\src\ReactChildFiber.new.js
function reconcileChildFibers(..., newChild, ...) {
// 根据新的child类型,进行不同的处理
if (typeof newChild === 'object' && newChild !== null) {
// 1,单个子节点处理
...
// 2,数组节点的处理,即多个子节点,比如div.App下面有三个子节点
// 循环创建多个子节点,最后返回第一个子节点,firstChild
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 3,处理文本子节点
...
// newChild为null时,表示没有子节点了,会返回一个null,由此一组beginWork工作执行完成,
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
进入reconcileChildrenArray
方法:
js
// packages\react-reconciler\src\ReactChildFiber.new.js
// 多个子节点创建
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
// 结果child
let resultingFirstChild: Fiber | null = null;
// 上一个新建的child
let previousNewFiber: Fiber | null = null;
// 旧的Fiber节点
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// 初次加载的情况:
if (oldFiber === null) {
// 多子节点的 创建:创建第一个的child,完成之后退出创建,返回resultingFirstChild结果为第一个child
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// previousNewFiber === null,表示为第一次循环:
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
// 设置第一个新建的Fiber节点, 为最后返回的child内容
resultingFirstChild = newFiber;
} else {
// 否则表示不是第一次循环,将新建的节点设置为上一个节点的兄弟节点
previousNewFiber.sibling = newFiber;
}
// 更新上一个新的节点
previousNewFiber = newFiber;
}
// 退出当前函数,返回创建完成的第一个child
return resultingFirstChild;
}
...
return resultingFirstChild;
}
创建多子节点的逻辑主要是一个for
循环结构:循环newChildren
数组,调用createChild
方法创建子节点。在第一个子节点创建完成之后,就会将它赋值为返回的child
节点,循环结构里只会赋值一次,所以reconcileChildrenArray
方法返回值就是第一个子节点。
js
resultingFirstChild = newFiber;
在后续的子节点创建过程中,会将新建的子节点设置为上一个节点的兄弟节点:
js
// 设置兄弟节点
previousNewFiber.sibling = newFiber;
处理完成后,从第一个子节点开始,就可以通过每个节点的sibling
属性指向下一个兄弟节点,一直到最后一个兄弟节点。
总结(案例)
上面我们以HostFiber
节点为例,解析了它的beginWork
工作流程。下面以前面的案例为目标,加快一组Fiber
节点的beginWork
工作:
js
// 从hostFiber根节点开始,【一组beginWork工作】
hostFiber =>
fun App() =>
div.App =>
div react源码调试 =>
null
从hostFiber
根节点开始,react的深度优先遍历算法,会一直创建到再无Child
子节点。
js
<div>react源码调试</div>
当【div react源码调试】所对应的Fiber
节点创建完成后,它会继续开启属于它的beginWork
工作,来创建它的子节点,但是因为它已经没有子节点了,所以在reconcileChildFibers
方法中,它最终会返回一个null
。
js
function reconcileChildFibers() {
...
// newChild为null时,表示没有子节点了,会返回一个null
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
然后一直向上return
,直到回到performUnitOfWork
方法中:
js
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate; // 上一次
// 开始循环构建
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
// 此next为null
next = beginWork(current, unitOfWork, subtreeRenderLanes);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
if (next === null) {
// next为null, 则会进入此节点对应的completeWork工作【归的阶段】
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
此时,next
为null
时,将进入此Fiber
节点的completeWork
工作【归的阶段】。
关于
FiberTree
的具体执行流程,也就是beginWork
和completeWork
的交替执行,可以查看4小节中的【执行流程图】。
4,completeWork
completeUnitOfWork
completeWork
代表一个Fiber
节点的结束工作,completeUnitOfWork
是completeWork
阶段的入口函数。
查看completeUnitOfWork
源码:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
function completeUnitOfWork(unitOfWork: Fiber): void {
# 取出当前的Fiber节点
let completedWork = unitOfWork;
// do while循环,先执行一次循环体,再判断
do {
// 取出节点 对应的current
const current = completedWork.alternate;
// 取出节点的父级节点
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
# 正常的completeWork工作
let next = completeWork(current, completedWork, subtreeRenderLanes);
// 归阶段,是自下而上的收缩,从div react源码解析 到fun App() 到 hostFiber节点
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
} else {
# 异常处理
// 因为某些异常原因返回了null,但是Fiber工作还没有完成,
const next = unwindWork(current, completedWork, subtreeRenderLanes);
// fiber树还没有完成,不能重置lanes
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
...
}
# 一个节点的completeWork工作完成后,寻找是否存在兄弟节点
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
// 如果兄弟节点存在,则设置为新的workInProgress,跳出当前函数,回到performUnitOfWork方法中
// 这将开始sibling兄弟节点的beginWork工作【自上而下】
workInProgress = siblingFiber;
return;
}
// 【自下而上的处理】
// 如果不存在兄弟节点,则将父级节点设置为completedWork,开始父级节点的completedWork工作
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
// 更新workInProgress,可能会开启新的beginWork工作
workInProgress = completedWork;
// 最后当completedWork为null时,就会跳出循环,这时候workInProgress也为null,代表整颗FiberTree就创建完成了
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
// 等于完成的状态
workInProgressRootExitStatus = RootCompleted;
}
}
completeUnitOfWork
方法中重点就是一个do while
循环:
js
completedWork !== null
这个循环的条件是completedWork
不等于null
,即它是一个有效的Fiber
节点。
接着上一节的执行结果,当前的completedWork
为【div react源码调试】所对应的Fiber
节点:
取出当前Fiber
节点的current
和父级节点,进入当前的节点的completedWork
工作。
关于completeUnitOfWork
方法中剩下的逻辑等待后续讲解。
completedWork
查看completedWork
方法:
js
// packages\react-reconciler\src\ReactFiberCompleteWork.new.js
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// 取出新的props
const newProps = workInProgress.pendingProps;
popTreeContext(workInProgress);
# 根据tag值,进行不同的逻辑处理
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
// 类组件
case ClassComponent: {
const Component = workInProgress.type;
if (isLegacyContextProvider(Component)) {
popLegacyContext(workInProgress);
}
bubbleProperties(workInProgress);
return null;
}
// 根节点的处理
case HostRoot: {}
# 原生dom节点处理
case HostComponent: {}
...
}
}
completedWork
方法源码量非常大,但是它的逻辑依然是熟悉的switch case
结构,根据Fiber
节点的tag
值进行不同的组件处理。
不同的
tag
值对应不同组件生成的Fiber
节点,前面已经讲解过。
当前Fiber
节点的tag
值对应着HostComponent
:
注意:这里
completeWork
阶段的工作我们以HostComponent
类型【dom元素】为案例,因为beginWork
阶段的重点是常规组件的加载逻辑,而completeWork
阶段的重点是HostComponent
类型组件的处理【创建真实的DOM结构,初始化属性及样式】。
js
case HostComponent: {
popHostContext(workInProgress);
// 获取#root 根元素
const rootContainerInstance = getRootHostContainer();
// 获取dom元素类型 'div'
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
# 更新元素
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
# 初次加载
if (!newProps) {
if (workInProgress.stateNode === null) {
throw new Error(
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
}
// This can happen when we abort work.
// props冒泡
bubbleProperties(workInProgress);
return null;
}
# 创建元素流程:
// 创建HostComponent类型的FIber节点 对应的真实dom元素
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 添加子元素:将下一级的dom元素挂载到instance上面
// 【由于appendAllChildren方法的存在,当completeWork到hostFiber时,已经形成了一颗离屏的真实DOM树】
appendAllChildren(instance, workInProgress, false, false);
// 在【div react源码解析】节点上: 存储它对应的真实dom内容
workInProgress.stateNode = instance;
// 执行finalizeInitialChildren方法,完成属性的初始化:styles,innerHTML, 文本类型的children
if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
// props冒泡,将flags冒泡
bubbleProperties(workInProgress);
return null;
}
HostComponent
组件的处理逻辑主要有两部分内容:
- 根据是否存在
current
判断为更新元素或者创建元素。 flags
冒泡。
当前我们的Fiber
节点为初次加载,current
为null
,所以会进入else
分支,进行真实dom
元素的创建。
js
const newProps = workInProgress.pendingProps;
newProps
取之于节点的pendingProps
属性,所以当前节点存在有效的props
,并且这里我们也可以看出本文内容被存储到了children
属性中。
继续下面的内容,调用createInstance
创建真实的DOM元素。
createInstance
查看createInstance
方法:
js
// packages\react-dom\src\client\ReactDOMHostConfig.js
// 创建真实的dom元素
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
const parentNamespace = ((hostContext: any): HostContextProd);
# 创建dom元素实例
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
// 更新fiber节点的props
updateFiberProps(domElement, props);
# 返回dom元素
return domElement;
}
createInstance
方法的作用是创建真实的DOM元素,最后返回创建完成的DOM元素。
继续查看createElement
方法:
js
// packages\react-dom\src\client\ReactDOMComponent.js
export function createElement(
type: string,
props: Object,
rootContainerElement: Element | Document | DocumentFragment,
parentNamespace: string,
): Element {
// 获取Document对象
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
if (namespaceURI === HTML_NAMESPACE) {
if (type === 'script') {
...
} else if (type.is === 'string') {
...
} else {
# 常规的dom元素
domElement = ownerDocument.createElement(type);
}
} else {
// svg/xml等元素的创建
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
# 返回创建完成的真实dom元素
return domElement;
}
createElement
方法主要是根据type
参数,使用原生方法document.createElement(type)
创建真实的DOM元素。
如果
namespaceURI
不等于HTML命名空间,则会使用document.createElementNS()
创建svg/xml等元素。
当前传递type
为div
类型:
js
const domElement = document.createElement('div')
最后返回创建完成的div
元素。
继续回到前面的createInstance
方法:
js
export function createInstance() {
...
# 创建的dom元素实例
const domElement: Instance = createElement();
// 将当前的Fiber节点存储到domElement元素上
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
# 返回dom元素
return domElement;
}
在创建完成真实的dom元素之后,做了两个挂载操作:
- 调用
precacheFiberNode
方法,在domElement
元素上定义一个react
内部属性存储当前的Fiber
节点。 - 调用
updateFiberProps
方法,在domElement
元素上定义一个react
内部属性存储当前Fiber
节点的pendingProps
。
最后返回domElement
。
回到HostComponent
处理逻辑中:
js
case HostComponent: {
...
# 真实的dom元素创建完成
const instance = createInstance();
# 添加子元素:将下一级的dom元素挂载到instance上面
// 【由于appendAllChildren方法的存在,当completeWork到hostFiber时,已经形成了一颗离屏的真实DOM树】
appendAllChildren(instance, workInProgress, false, false);
// 在div react源码解析 节点上 存储它对应的真实dom元素
workInProgress.stateNode = instance;
// 执行finalizeInitialChildren方法,完成属性的初始化:styles,innerHTML, 文本类型的children,
if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
# props冒泡,将flags冒泡
bubbleProperties(workInProgress);
return null;
}
在真实的dom
创建完成之后,继续调用appendAllChildren
,它的作用是将下一级的dom
内容添加到当前的DOM元素之中。
appendAllChildren
查看appendAllChildren
方法:
js
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
let node = workInProgress.child;
// 有child才会处理,无child,说明自身就已经最下面的节点了
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
};
根据appendAllChildren
方法名称其实就已经知道它的作用了,将当前FIber
节点下的所有子节点对应的dom
内容添加到当前的DOM元素之中。
js
let node = workInProgress.child;
取出当前FIber
节点的子节点【fisrtChild
】,只有存在子节点才会开启while
循环,当前workInProgress
为【div react源码调试】所对应的Fiber
节点,它已经是最下面的节点【即不存在子节点】,所以这里的appendAllChildren
方法没有处理内容。
案例解读
举例: 假如当前的节点为【state: {count}
】所对应的FIber
节点:
前面我们已经知道,因为这个节点内容存在动态数据,所以它还会继续创建两个子节点。
当这个FIber
节点进入appendAllChildren
方法时,它存在child
就可以正常的进入while
循环:
【state:】所对应的Fiber
节点tag
值为6,代表它是文本节点内容,满足条件判断:
js
appendInitialChild(parent, node.stateNode);
// 即是原始dom方法
div.appendChild(text)
执行原生DOM
方法,将文本子节点添加到父级节点之下。当这行代码执行完成之后,【state: {count}
】所对应的FIber
节点,它所对应的DOM元素就已经发生了变化:
js
<div></div>
// 变成了
<div>state: </div>
继续回到while
循环中:
js
// 有child才会处理,无child,说明自身就已经最下面的节点了
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
// 添加子节点
appendInitialChild(parent, node.stateNode);
} else if (node.tag === HostPortal) {
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
# 更新node 为兄弟节点
node = node.sibling;
}
在第一个子节点处理完成后,更新node
对象:
js
node = node.sibling;
将下一个兄弟节点设置为最新的node
,如果node
存在,说明当前还存在其他子节点,继续循环添加子节点。
开启第二次循环,继续执行原生DOM
方法添加子节点:
当这行代码执行完成之后,【state: {count}
】所对应的FIber
节点,它所对应的DOM元素就再次更新:
js
<div>state: </div>
// 变成了
<div>state: 1</div>
到此,该FIber
节点对应DOM元素的更新就已经处理完成,因为它只有两个子节点。
js
while (node.sibling === null) {
if (node.return === null || node.return === workInProgress) {
return;
}
}
再遇到此判断就会触发return
关键字,跳出appendAllChildren
方法,表示执行完成。
当这个方法执行完成后,在HostComponent
处理逻辑中,就会将最新的DOM
内容存储到当前Fiber
节点的stateNode
属性中。
上面通过一个案例,帮助我们更好的理解了appendAllChildren
方法的内容。
下面我们再次回到之前HostComponent
处理逻辑中:
js
case HostComponent: {
...
const instance = createInstance();
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
# 完成DOM元素属性的初始化,以及原生事件的绑定
if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
markUpdate(workInProgress);
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
# props冒泡,将flags冒泡
bubbleProperties(workInProgress);
return null;
}
在DOM内容更新完成后,然后调用了一个finalizeInitialChildren
方法,这个方法有两个作用:
- 初始化当前
DOM
元素的原生属性。 - 添加原生事件绑定。
finalizeInitialChildren
查看finalizeInitialChildren
方法:
js
// packages\react-dom\src\client\ReactDOMHostConfig.js
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 初始化dom属性【重点】
setInitialProperties(domElement, type, props, rootContainerInstance);
switch (type) {
case 'button':
case 'input':
case 'select':
case 'textarea':
return !!props.autoFocus;
case 'img':
return true;
default:
return false;
}
}
finalizeInitialChildren
方法中最重要的逻辑就是setInitialProperties
方法的调用,而下面的switch case
结构只是针对几个特殊的DOM元素,打上一个更新的标记,用于在commit
阶段之中对特殊的元素进行预加载处理,比如输入聚焦或者图片加载。
这里还是回到之前的【div react源码调试】所对应的Fiber
节点的执行逻辑。
上一小节展示【
state: {count}
】所对应的FIber
节点案例,只是为了方便解读appendAllChildren
方法内容。
setInitialProperties
方法传递了四个参数:
- 当前DOM元素。
- 元素类型
div
。 - 定义的
props
。 #root
应用根节点元素。
继续查看setInitialProperties
方法:
js
// packages\react-dom\src\client\ReactDOMComponent.js
export function setInitialProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document | DocumentFragment,
): void {
const isCustomComponentTag = isCustomComponent(tag, rawProps);
let props: Object;
// 针对一些特殊的DOM元素,添加默认事件绑定
switch (tag) {
case 'dialog':
listenToNonDelegatedEvent('cancel', domElement);
listenToNonDelegatedEvent('close', domElement);
props = rawProps;
break;
case 'iframe':
case 'object':
case 'embed':
listenToNonDelegatedEvent('load', domElement);
props = rawProps;
break;
case 'video':
case 'audio':
for (let i = 0; i < mediaEventTypes.length; i++) {
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
}
props = rawProps;
break;
case 'source': // ...
break;
case 'img':
case 'image':
case 'link': // ...
break;
case 'details': // ...
break;
case 'input': // ...
break;
case 'option': // ...
break;
case 'select': // ...
break;
case 'textarea': // ...
break;
default:
props = rawProps;
}
// props合法校验
assertValidProps(tag, props);
# 绑定真实的属性和事件
setInitialDOMProperties(
tag,
domElement,
rootContainerElement,
props,
isCustomComponentTag,
);
}
setInitialProperties
方法内容主要有三个逻辑步骤:
- 针对一些特殊的
DOM
元素,添加必要的事件绑定。 - 对传入的
props
进行合法性校验。 - 调用
setInitialDOMProperties
方法,根据props
中的内容,对当前的DOM元素进行真实的属性和事件绑定。
setInitialDOMProperties
方法具体的内容就不再展开了,这部分逻辑涉及到了React合成事件系统的原理,里面的内容比较复杂,感兴趣的可以自行了解。
对finalizeInitialChildren
方法做一个总结:根据props
中的内容,对当前的DOM
元素进行真实的属性和事件绑定。
根据前面的图例展示:当前的Fiber
节点对应的DOM元素为div
,且只有一个children
属性,并没有其他属性和事件绑定,所以
这里仅仅是设置了元素的textContent
文本属性之后,就没有其他逻辑执行了。
上面的内容执行完成后,再次回到HostComponent
处理逻辑中,最后执行了一个bubbleProperties
方法,这个方法的作用是执行flags
冒泡,让子节点的flags
向上冒泡一层。经过冒泡处理后,访问任意一个Fiber
节点的subTreeFlags
属性,都可以获取它的子节点所需要执行的副作用操作。
flags
标记对应不同的副作用操作,用于commit
阶段的renderer
渲染器执行对应的操作逻辑。
总结(案例)
上面HostComponent
相关的一系列逻辑执行完成后,即代表了一个Fiber
节点的completedWork
工作就此完成,这里以HostComponent
为例,主要是处理DOM的相关逻辑才是completedWork
工作的主要内容,其他的组件类型基本都只是处理flags
冒泡。
在一个Fiber
节点的completedWork
工作完成后,最终都是回到completeUnitOfWork
方法中。
下面我们再次回到此方法,解析后续的逻辑执行:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
// 完成当前的Fiber节点,然后移动到兄弟节点,如果没有,则返回到父级节点
function completeUnitOfWork(unitOfWork: Fiber): void {
# 取出当前的Fiber节点
let completedWork = unitOfWork;
# do while循环,先执行一次循环体,再判断
do {
// 取出节点 对应的current
const current = completedWork.alternate;
// 取出节点的父级节点
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
# 正常的completeWork工作
let next = completeWork(current, completedWork, subtreeRenderLanes);
// 归阶段,是自下而上的收缩,从div react源码解析 到fun App() 到 hostFiber节点
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
} else {
# 异常处理
// 因为某些异常原因返回了null,但是Fiber工作还没有完成,
const next = unwindWork(current, completedWork, subtreeRenderLanes);
// fiber树还没有完成,不能重置lanes
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
...
}
# 一个节点的completeWork工作完成后,寻找是否存在兄弟节点
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果兄弟节点存在,则设置为新的workInProgress,跳出当前函数,回到performUnitOfWork方法中
// 这将开始sibling兄弟节点的beginWork工作【自上而下】
workInProgress = siblingFiber;
return;
}
// 【自下而上的处理】
// 如果不存在兄弟节点,则将父级节点设置为新的completedWork,开始父级节点的completedWork工作
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
// 更新workInProgress,可能会开启新的beginWork工作
workInProgress = completedWork;
// 最后当completedWork为null时,就会跳出循环,这时候workInProgress也为null,代表整颗FiberTree就创建完成了
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootInProgress) {
// 等于完成的状态
workInProgressRootExitStatus = RootCompleted;
}
}
当前Fiber
完成completedWork
工作后,返回了一个null
:
js
let next = completeWork(current, completedWork, subtreeRenderLanes); // null
然后继续进入下面的程序,首先判断当前节点是否存在兄弟节点sibling
:
js
const siblingFiber = completedWork.sibling;
根据我们准备的案例,很明显它是存在兄弟节点的:
存在兄弟节点,则将兄弟节点设置为最新的workInProgress
,触发return
关键字,退出completeUnitOfWork
函数。
这将会回到workLoopSync
方法中,此时workInProgress
为有效的内容,将开始【MyFun
函数组件】Fiber
节点的benginWork
工作。
到处,关于一个Fiber
节点的beginWork
和completedWork
工作流程就基本解析完成了。
最后再对react
应用创建FiberTree
的过程做一个总结:react中FiberTree
的创建过程是采用DFS深度优先遍历,从beginWork
方法开始深度探索阶段【自上而下】,也就是处理每层第一个child
【firstChild
】,直到没有子节点。然后进入completeWork
方法【归的阶段,自下而上】,在这里继续处理一些该Fiber
节点的工作,在这个过程中两个工作会交替执行 。beginWork
工作从HostFiber
开始,最终由completeWork
回到HostFiber
,因为HostFiber
是FiberTree
的根节点,它没有父级节点,所以在completeWork
方法最后会返回一个null
,赋值给workInProgress
。上面所有的工作执行完成后,最后会回到workLoopSync
方法中,此时workInProgress
为null
,就会跳出循环代表本次FiberTree
已经创建完成。
执行流程图
下面是案例对应的完整执行流程图【可与前面的FiberTree
结构图对比】:
5,结束语
上面我们已经学习了FiberTree
整体的创建过程,下一章节我们将进入react的commit
阶段,开始真正的页面渲染以及相关副作用的执行过程。
关于类组件和函数组件的具体加载过程,可以查看《React18.2x源码解析:组件的加载过程》。