最近在看react
应用更新时的一些逻辑处理,因为之前的fiber reconciler
协调流程重点学习的是FiberTree
的创建过程,对整个更新流程没有进行深入的了解。本次在学习应用更新时也对函数组件性能优化有了更加深刻的认识,所以也是趁着有时间整理了一下文档,记录一下关于函数组件性能优化的相关内容。
本次内容会分成以下三个章节来讲解函数组件性能优化的逻辑原理:
- 普通函数组件更新。
memo
组件更新。- 性能优化使用总结。
本章节将讲解普通函数组件的更新逻辑,因为我们只有先知道普通函数组件更新特点,才能更好地理解下一章节React.memo
方法是如何来优化普通函数组件的。
1,案例准备
首先我们准备一个函数组件更新的测试案例:
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 React from 'react'
import MyFun from './views/MyFun';
export default function App() {
console.log('App组件运行了')
return (
<div className="App">
<div>react源码调试</div>
<MyFun name='MyFun'></MyFun>
</div>
);
}
js
// MyFun.js
import { useState } from 'react'
import Child from './Child'
export default function MyFun(props) {
console.log('MyFun组件运行了')
const [count, setCount] = useState(1)
function handleClick() {
setCount(count + 1)
}
return (
<div className='MyFun'>
<div>state: {count}</div>
<Child name='Child'></Child>
<button onClick={handleClick}>更新</button>
</div>
)
}
js
// Child.js
export default function Child(props) {
console.log('Child组件运行了')
return (
<div className='Child'>
<div>Child子组件</div>
<div>name: {props.name}</div>
</div>
)
}
案例加载完成的页面内容:
Fiber树结构
案例对应的Fiber
树结构:
这里没有展开Child
组件内部的Fiber
结构,因为我们这里关注的是它组件节点的更新。
注意: 案例唯一的更新按钮在MyFun
组件中:
- 它的父组件
App
自身没有状态,也没有给MyFun
传递动态props
。 - 它的子组件
Child
自身没有状态,也没有给它传递动态props
。
下面我们来看MyFun
组件更新,到底会对它的父组件和子组件造成什么样的影响。
2,触发更新
点击更新按钮,查看打印结果:
可以发现MyFun
组件重新渲染了,它的子组件Child
也重新渲染了,它的父组件App
没有重新渲染。
下面我们就来分析这整个的更新流程。
触发状态变化
点击更新按钮,就会触发DOM事件回调,然后执行我们的状态修改操作:
js
function handleClick() {
setCount(count + 1)
}
此时就会来到useState
对应的dispatchSetState
方法中:
该方法会执行以下几个步骤:
- 创建一个
update
更新对象。 - 将
update
对象添加到queue
队列中,形成一个单向环状链表。 - 调用
scheduleUpdateOnFiber
方法,开启一个新的调度更新任务。
注意: 由useState hook
的dispatch
方法触发的状态变化,在触发调度之前,会有一个eagerState
优化策略校验,也就是快速计算出本次状态变化的结果,与原来的状态进行对比,判断是否发生了真的状态变化:
- 如果没有发生变化,则会进入
eagerState
策略,不会发起调度更新任务。 - 如果发生了变化,则开启一个新的调度更新任务。
js
if (is(eagerState, currentState)) {
/**
* 如果相等,则满足优化策略,只是将update更新对象添加到queue队列,不会开启新的更新调度。
* 【注意:】这里是针对单次修改state做的eagerState优化策略:本次修改无变化,就不发起调度更新【只需要将update对象添加到临时链 * 表即可,等会真正变化的dispatchSetState来触发调度更新】
* updateReducer里面的Bailout策略是针对一个hook对象的整个update链表计算完成后,即一个state多次修改最终计算之后,再判断
* state有没有变化:1,有变化进行正常的组件更新; 2,无变化则进入Bailout优化策略。
*
*/
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
注意: eagerState
策略校验针对的是单次修改状态的校验。
js
function handleClick() {
// 每次修改都会执行一次校验
setCount(1)
setCount(2)
}
比如在DOM事件中一次性执行多次状态修改,则每次dispatch
方法都会执行一次eagerState
的策略校验。
当前我们的状态发生了真实的变化,所以就会发起一个新的调度更新任务,触发一次应用的更新流程。
由点击事件触发的更新任务为同步优先级,但是react
会默认使用微任务来处理同步任务。
最终会来到workLoopSync
方法中,开始创建Fiber
树,也就是应用更新的核心流程。
前面省略了一部分调度更新的逻辑,因为这部分不是本章的重点。关于这部分的详细逻辑可以查看《React18.2x源码解析:函数组件的加载过程》,这里不会过多关注。
根节点的处理
在解析workLoopSync
方法之前,我们还得先知道Fiber
树根节点的处理。
注意: react
应用的每次更新渲染都会构建一颗新的Fiber
树,也就是说构建Fiber
树的过程就是应用更新的核心流程。
在这个流程中每个Fiber
节点都会执行两项工作:
beginWork
工作completeWork
工作
组件节点的工作重点在beginWork
中,普通DOM节点的工作重点在completeWork
中。
HostFiber
节点是Fiber
树的根节点【可以看前面的树结构】,每次构建Fiber
树的工作都是从这个根节点开始的,而HostFiber
是在workLoopSync
方法执行之前就已经创建完成的,也就是说在每次构建Fiber
树之前它的根节点就已经确定了。所以workLoopSync
第一个处理的就是HostFiber
节点,开始执行它的beginWork
工作。
在开始它的beginWork
工作之前,我们得先知道HostFiber
的创建,因为这个非常重要。
在执行workLoopSync
方法之前,会调用一个prepareFreshStack
方法来做本次更新的准备工作。
prepareFreshStack
js
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
...
// 使用旧的hostFiber节点,来创建一个工作中的hostFiber,即WorkInProgress【新的】
const rootWorkInProgress = createWorkInProgress(root.current, null);
// 并且将全局变量:workInProgress赋值为 新的hostFiber节点
workInProgress = rootWorkInProgress;
...
}
这个准备工作首先一点就是:确定了本次构建Fiber
树的根节点,也就是hostFiber
。
即调用createWorkInProgress
方法来创建一个新的Fiber
节点,因为当前传递的参数是root.current
,它的意思就是根据旧的hostFiber
节点来创建新的hostFiber
。并且将这个节点赋值给全局变量workInProgress
,所以下面我们进入workLoopSync
方法时,全局变量workInProgress
存储的就是hostFiber
根节点对象。
这里我们需要再关注一下createWorkInProgress
方法:它是更新阶段一个通用的创建Fiber
节点的方法 ,并不只是用于创建hostFiber
节点,其他的Fiber
节点更新时也会用到这个方法。
createWorkInProgress
js
// packages\react-reconciler\src\ReactFiber.new.js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
/**
* 该Fiber节点第一次更新的情况:
* 它的alternate属性为null,即workInProgress为null。需要调用FiberNode构造函数创建新的Fiber节点,
* 然后复用current节点信息
*/
workInProgress = createFiber(
current.tag,
pendingProps, // 复用props
current.key,
current.mode,
);
// 复用相关信息
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
/**
* 表示该Fiber节点的第二次更新及以上:
* 不需要再使用FiberNode构造函数来创建Fiber对象了,直接复用current节点相关的信息
*
*/
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
}
# 复用通用的current节点信息
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;
# 返回新的Fiber节点
return workInProgress;
}
我们首先查看这个方法的两个参数:
current
:旧的Fiber
节点。pendingProps
:新的props
对象。
根据这两个参数我们就可以得知这个方法的作用:复用current
旧的节点信息以及新的props
对象来创建对应的新的Fiber
节点。
虽然这个方法看着内容很多,但是它的核心就是一个if else
结构,下面我将它的逻辑整理一下:
js
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 表示该Fiber节点第一次更新
workInProgress = createFiber(
current.tag,
pendingProps, // Fiber节点的beginWork工作时,就会判断新旧props
current.key,
current.mode,
);
...
} else {
// 表示该Fiber节点非第一次更新
workInProgress.pendingProps = pendingProps;
...
}
# 复用通用信息
...
// 返回新的节点
return workInProgress
}
整理之后我们就可以发现它的逻辑并不复杂,这里根据当前Fiber
节点的alternate
属性值判断当前节点是否为第一次更新:
- 为
null
时,代表当前Fiber
节点为第一次更新,需要使用FiberNdoe
构造函数创建新的Fiber
实例,然后再复用旧的节点信息。 - 不为
null
时,代表当前Fiber
节点不是第一次更新,直接复用旧节点的相关信息。
在if else
结构之后是复用旧节点的一些通用信息,最后返回对应的新的Fiber
节点。
注意: 此方法的核心在于通过复用旧的节点信息来创建新的
Fiber
节点,至于此节点是多少次更新并不是重点。
虽然createWorkInProgress
方法内部复用了旧节点的很多属性内容,但是我们这里的重点是关注它的第二个参数:pendingProps
。
也就是Fiber.pendingProps
属性的设置,它代表该节点新的props
数据,此属性会影响该节点更新时的beginWork
工作中能否进入Bailout
策略,后面就会讲解这部分逻辑。
比如当前创建新的hostFiber
节点时,第二个属性默认为null
:
js
const newFiber = createWorkInProgress(root.current, null);
也就是说:所以无论多少次更新,hostFiber
节点的pendingProps
属性都是null
。
以上就是createWorkInProgress
方法的主要内容,下面我们就正式进入workLoopSync
方法,开始Fiber
树的构建。
3,更新流程
执行流程图
beginWork
工作执行流程图:
下面我们会通过分析几个重要的Fiber
节点来学习react
的更新流程逻辑。
HostFiber节点
通过上面我们已经知道了第一个执行的是hostFiber
根节点:
下面我们直接来到它的beginWork
工作:
js
beginWork(current, workInProgress, lanes)
js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
# 更新流程
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 主要判断:props变化和context上下文变化
if (oldProps !== newProps || hasLegacyContextChanged() ) {
// props有变化或者上下文有变化时,Fiber节点自身需要更新就会进入这里
didReceiveUpdate = true;
} else {
// props和上下文都没有变化的情况,检查当前Fiber节点自身是否有挂起的调度更新或者上下文修改
// 在修改state触发的更新,这里检查的结果就会为true
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
// hasScheduledUpdateOrContext为false,即Fiber自身没有更新的情况下:进入Bailout策略
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
// 当前Fiber存在更新的情况:需要进入正常的组件更新流程,重新调用组件函数,生成新的react元素对象
didReceiveUpdate = false;
}
} else {
# 加载流程
}
workInProgress.lanes = NoLanes;
// 根据Fiber.tag值走不同的组件处理逻辑
switch(workInProgress.tag) {
case FunctionComponent: {}
case ClassComponent: {}
...
}
}
beginWork
方法通过判断current
是否有值来区分此节点是加载还是更新,当前我们是更新流程,current
都是有值的,直接查看更新逻辑。不过在这里还是要提前说明一下:在react中,影响一个Fiber
节点更新的只有这三种因素:
state
props
context
首先取出新旧Fiber
节点的对应的props
对象,即新旧props
数据:
js
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
然后判断新旧props
是否相等,前面已经说了hostFiber
节点的props
默认是null
,所以这里是相等的。
js
if (oldProps !== newProps || hasLegacyContextChanged() ) {
// props变化的情况:
didReceiveUpdate = true;
} else {
// props无变化的情况:
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
// 判断有没有等待的更新
if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
didReceiveUpdate = false;
// 无更新,进入Bailout策略
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
// 有更新,进入正常的更新流程
didReceiveUpdate = false;
}
注意: 这里虽然有context
上下文的变化检查,它针对的是旧版的LegacyContext
,新版的context
变化表现在lanes
中,所以第一部分的判断我们主要关注props
的变化即可。
在props
和LegacyContext
都无变化的时候,进入else
分支处理:
js
// 检查是否存在调度更新或者context变化
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
js
function checkScheduledUpdateOrContext(current: Fiber, renderLanes: Lanes,): boolean {
// 检查是否存在更新
const updateLanes = current.lanes;
// 检查当前节点lanes,有没有renderLanes
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
return false;
}
继续检查当前Fiber
节点是否有state
变化触发的更新或者context
变化。
- 这里检查的是
current
节点的lanes
,因为组件在修改state
触发更新时,会将renderLanes
绑定到在current
节点之上。 - 同时这里也检查了
context
的变化,因为新版context
在发生变化时也会为Consumer
对应的Fiber
节点的lanes
属性绑定renderLanes
,所以这里通过Fiber.lanes
的值同时检查了Fiber
节点可能会出现的两种更新。
然后定义一个变量hasScheduledUpdateOrContext
接收校验的结果:
- 如果结果为
false
,代表不需要更新,设置全局变量didReceiveUpdate
为false
,然后进入Bailout
策略逻辑,执行更进一步的优化程度判断。 - 如果结果为
true
,表示有更新内容,但注意的是:有更新并不意味着state
确实发生了变化,比如一个组件多次修改了state
,它存在更新的任务,但有可能多次状态修改的最终结果是没有变化,所以只有在更新时计算state
后明确发生了真正的状态变化,才会同步更新这个变量的值为true
。所以这里还是设置的didReceiveUpdate
为false
,进入下面的switch case
结构,执行对应的组件更新逻辑,之后再根据计算后的状态值再次判断是否进行真正的更新。
同时在这里我们也可以发现,react
内部存在默认的优化策略 :即在每个Fiber
节点的beginWork
工作中,通过一系列的判断来校验当前Fiber
节点是否可以进入Bailout
策略,来跳过该节点的更新工作。
当前我们的hostFiber
节点满足Bailout
策略,进入attemptEarlyBailoutIfNoScheduledUpdate
方法。
js
function attemptEarlyBailoutIfNoScheduledUpdate() {
...
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
直接查看最后一行代码,继续调用了一个Bailout
策略方法:
js
// Bailout策略核心方法
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
workInProgress.dependencies = current.dependencies;
}
markSkippedUpdateLanes(workInProgress.lanes);
/***
*
* 检查子节点是否存在有更新的工作:这里应该是判断该组件的整个子树有没有更新工作?
* 1,如果没有则可以直接跳过整个组件的更新流程
* 2,如果有,则直跳过组件自身节点的更新流程
*
**/
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
// 来到这里:表示当前Fiber节点没有更新工作,但是它的子节点有,所以只能跳过该节点自身的更新流程
// 在更新阶段,hostFiber,App根组件节点这些节点自身没有变化基本都会走这里
cloneChildFibers(current, workInProgress);
// 比如当前workInProgress为HostFiber, 它的child就是App根组件节点
// 下一步就是开始新的App组件节点的beginWork工作
return workInProgress.child;
}
这个方法是Bailout
优化策略的核心方法,它的作用执行进一步的优化程度判断:
- 高度优化:跳过整个子树的更新流程。
- 一般优化:跳过自身节点的更新流程。
在这里我们还要了解Fiber
节点上两个属性的区别:
lanes
:如果存在renderLanes
,则说明节点自身存在更新。childLanes
:如果存在renderLanes
,则说明该节点的子树存在更新。
在进入Bailout
策略之前,已经对Fiber.lanes
进行了校验。而能够进入Bailout
策略,已经说明节点自身是不存在更新需求的,所以bailoutOnAlreadyFinishedWork
方法内只需要再对childLanes
进行校验,就可以判断出最终的优化程度了。
这里检查当前节点的childLanes
判断子树是否存在更新的工作:
- 如果没有:则可以直接跳过该节点整个子树的更新流程。
- 如果有:则只跳过该节点自身的更新流程。
在更新流程中:Fiber
树的根节点一般都不会存在更新,但是它的子树一定是存在更新的,比如当前的MyFun
组件节点。
所以这里不满足高级优化的条件,就会执行cloneChildFibers
方法,通过克隆复用来生成它的下一层子节点。
js
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
if (current !== null && workInProgress.child !== current.child) {
throw new Error('Resuming work not yet implemented.');
}
if (workInProgress.child === null) {
return;
}
// 注意:current.child和workInProgress.child是指向的同一个child。
// 这里的目的是通过现在的child内容,创建另外一个即alternate属性指向的新Fiber节点
let currentChild = workInProgress.child;
# 复用生成新的Fiber节点
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
// 比如当前的workInProgress是hostFiber时,就会创建新的App组件节点,然后更新hostFiber.child
workInProgress.child = newChild;
newChild.return = workInProgress;
# 如果旧child还有兄弟节点,则循环创建它们对应workInProgress
// 比如<div>react源码调试</div>它有兄弟阶段MyFun函数组件,此时就会继续新建它的Fiber节点
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
// 无兄弟节点,执行结束
newChild.sibling = null;
}
cloneChildFibers
方法的作用就是克隆下一层子节点,比如当前hostFiber
的子节点就只有一个App
组件,所以这里只需要克隆生成App
组件对应的新的Fiber
节点即可,同时这里我们也可以看到这个方法的内部也是调用的createWorkInProgress
方法:
js
let currentChild = workInProgress.child;
// 创建新的Fiber节点
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
// 比如当前的workInProgress是hostFiber时,就会创建新的App组件节点,然后更新hostFiber.child
workInProgress.child = newChild;
前面已经讲过这个方法,它的作用就是:复用旧的节点信息生成新的Fiber
节点,当前就是生成一个属于App
组件的新的Fiber
节点,然后更新hostFiber
节点的child
属性为新的App
组件节点。
js
return workInProgress.child;
最后返回子节点即App
组件节点,开始App
组件节点的beginWork
工作。
App组件节点
下面再次来到App
组件节点的beginWork
工作处理:
注意: App
组件节点是通过克隆复用旧的节点信息生成的,它的props
对象也是原props
对象,即新旧props
数据相等,所以这里App
组件节点依然可以进入Bailout
策略,会执行跟HostFiber
节点一样的处理逻辑。
最后还是会来到cloneChildFibers
方法中,生成App
组件节点的子节点【div.App
】。
【div.App
】节点也是一样的beginWork
工作逻辑,就不再重复说明了。这里我们加快进度,直接来到cloneChildFibers
方法中,再次来到该方法中时,因该节点有两个子节点,所以在第一个子节点生成之后,判断发现它的sibling
属性有值,就会循环继续生成它的兄弟节点,即我们的MyFun
函数组件节点。
后面的执行逻辑就是:
js
// 1,执行:div 源码调试 节点的beginWork工作
// 2,执行:div 源码调试 节点的completeWork工作
// 3,执行:MyFun 节点的beginWork工作
MyFun组件节点
下面我们直接来MyFun
组件节点的beginWork
工作。
因为MyFun
组件节点也是通过克隆生成的,所以它的新旧props
也是相等的。
但是重点来了: 本次的更新流程是由该组件修改state
触发的,所以该Fiber.lanes
含有renderLanes
即存在更新。
所以这里再调用checkScheduledUpdateOrContext
校验时就会返回true
,MyFun
函数组件节点就无法进入Bailout
策略。
js
if (oldProps !== newProps || hasLegacyContextChanged() ) {
// props变化的情况:
didReceiveUpdate = true;
} else {
// props无变化的情况:
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes);
// 判断有没有等待的更新
if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
// 无更新的情况:
}
# 有更新,进入正常的更新流程
didReceiveUpdate = false; // 还是设置为false
}
...
switch (workInProgress.tag) {
case FunctionComponent: {
return updateFunctionComponent() // 执行正常的函数组件更新
}
}
最后来到下面的switch case
结构,执行函数组件正常的更新流程。
下面查看updateFunctionComponent
方法:
js
// 更新函数组件
function updateFunctionComponent(
current,
workInProgress,
Component,
nextProps: any,
renderLanes,
) {
// 调用组件函数
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
/**
* 函数组件默认的更新优化策略:【极少:可以通过两次修改state,加一减一来测试】
* 一般情况下都无法进入此处的Bailout策略,除非是多次修改一个state情况,
* 同时这个state必须为原始数据类型,比如number或者布尔值,并且多次修改的最终结果是【无变化】。
* 这样的情况didReceiveUpdate才能保持false的值,然后进入此处的Bailout优化策略。
*/
if (current !== null && !didReceiveUpdate) {
// 多次修改state,虽然最终结果无变化。但是effect相关的hook,依赖也是变化了。
// 这里需要清除hook的副作用标记,表示不需要更新effect
bailoutHooks(current, workInProgress, renderLanes);
/**
* 如果能够进入Bailout策略:代表函数组件自身Fiber无需要更新
* 1,如果子节点也没有更新,则跳过整个组件的更新流程
* 2,如果子节点有更新,则跳过自身节点的更新,复用child子节点的内容;
*/
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 创建子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
updateFunctionComponent
方法就是专门处理函数组件的加载或者更新逻辑,这个方法的重点就是调用了一个renderWithHooks
方法,
js
function renderWithHooks() {
const children = Component() // 调用MyFun函数
}
renderWithHooks
方法里面的内容很多,但我们主要关注这一行代码,即调用我们定义的组件函数:
在调用我们定义的组件函数时,主要会执行以下几个内容:
- 遇到
useState hook
时,循环update
链表,计算最新的state
数据。 - 遇到
effect
副作用类的hook
时,如果依赖变化,则打上HookHasEffect
更新标记。 - 最后根据
return
返回的jsx
内容,生成MyFun
函数组件内所有节点对应的新的react
元素对象。
注意: 在state
计算完成后,会执行一个判断:
js
function updateReducer(reducer, initialArg, init) {
...
// state循环计算完成后
if (!objectIs(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
}
判断本次state
修改后最终的结果有没有变化,如果发生了变化则会调用一个markWorkInProgressReceivedUpdate
方法,更新全局变量didReceiveUpdate
为true
,代表确实需要执行真正的更新。
js
export function markWorkInProgressReceivedUpdate() {
didReceiveUpdate = true;
}
这就是为啥之前变量didReceiveUpdate
还是为false
的原因,要确保真实的状态变化才需要执行真正的更新。
关于
renderWithHooks
方法具体的执行逻辑可以查看《React18.2x源码解析:函数组件的加载过程》
renderWithHooks
方法执行完成后,返回值就是新的react
元素对象。
注意:调用
renderWithHooks
会为该组件内所有子节点创建对应的新的react元素对象。
js
// 加载函数组件
function updateFunctionComponent() {
// 调用组件函数
const nextChildren = renderWithHooks(
current,
workInProgress,
Component,
nextProps,
context,
renderLanes,
);
// Bailout策略校验
if (current !== null && !didReceiveUpdate) {
bailoutHooks(current, workInProgress, renderLanes);
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
// 重新渲染:创建子节点
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
在react
元素对象创建完成之后,就会根据此对象来创建函数组件内新的子节点。不过在执行之前,这里也有一个Bailout
优化策略校验,这里判断条件的重点就是didReceiveUpdate
变量,不过因为当前MyFun
函数组件确实发生了真实的状态变化,已经将didReceiveUpdate
更新为了true
,所以这里就不满足Bailout
策略,必须进入正常的更新流程。
到这里我们其实可以发现:函数组件类型的
Fiber
节点在执行beginWork
工作时,前后总共会执行两次Bailout
策略校验。这说明react内部是存在默认的优化策略的,类组件也有。
注意: 实际上想要满足函数组件内部默认的Bailout
优化策略,条件是比较苛刻的。首先必须是针对同一个state
进行多次修改,然后state
必须是原始数据类型,比如常见的字符串,数字和布尔值,并且必须保证多次修改的最终结果是无变化,这样才能进入默认的Bailout
策略。
js
function handleClick() {
setCount(state => state + 1)
setCount(state => state - 1)
}
比如我们执行这样的修改操作,虽然修改了多次state
,但是最终的计算结果缺没有变化,就可以进入函数组件内部的Bailout
策略,同时在这里还会清除由于state
依赖变化而被打上标记的副作用。
js
// 清除副作用标记
bailoutHooks(current, workInProgress, renderLanes);
回到案例,当前MyFun
组件发生了真实的状态变化,所以会调用reconcileChildren
方法创建新的子节点。
js
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
注意: 满足Bailout
策略时是通过cloneChildFibers
来创建子节点;不满足此策略时,即正常的组件更新是通过reconcileChildren
来创建子节点。虽然这两种方式最终都是调用的createWorkInProgress
方法来创建子节点,但是reconcileChildren
和cloneChildFibers
方法最大的不同就是第二个参数传递的值不同:
cloneChildFibers
: 传递的是current
旧节点的props
对象。reconcileChildren
:传递的是由新生成的的react
元素对象上的props
。
js
// cloneChildFibers
const newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 复用旧props
// reconcileChildren
const newChild = createWorkInProgress(fiber, pendingProps); // react元素对象传递的新的props对象
注意:一个Fiber
对应的新建的react
元素对象,它的props
绝对不会和原来props
对象相等,即使是空对象。
yaml
{} !== {}
学习到这里,我们就能知道MyFun
函数组件重新渲染,为何它的所有子节点也都会重新渲染了,因为这些子节点的pendingProps
属性都已经是新的props
对象了,在子节点执行它的beginWork
工作时:
js
if (oldProps !== newProps || hasLegacyContextChanged() ) {
// props发生变化,直接进入更新流程
didReceiveUpdate = true;
}
props
校验结果不相等,就会直接进入它的更新流程了。
Child组件节点
最后直接来看Child
组件的beginWork
工作流程:
根据调试截图可以发现,虽然Child
子组件新旧props
数据内容并没有实际的变化,但是此时的props
已经是一个新的对象了。
js
const oldProps = {name: 'child'};
const newProps = {name: 'child'};
if (oldProps !== newProps) // true
所以下面的新旧props
就不会相等了,判断结果为true
,直接进入该组件正常的更新流程。
这就是为何MyFun
组件更新,Child
子组件在无实际props
变化和自身状态变化的情况下也被动重新渲染了。
4,总结
1,在react中,一次更新流程的核心逻辑就是创建一颗新的Fiber
树,每个Fiber
节点的创建工作都分为beginWork
和completeWork
两个模块,在beginWork
工作开始时,每个Fiber
节点都会执行一次Bailout
优化策略校验。
2,在react中,影响一个Fiber
节点更新的有三种因素:
state
props
context
state
和context
的变化会将renderLanes
附加到该Fiber
节点的lanes
属性上。
3,Bailout
策略会依次校验这三个因素变化:
props
的变化:是直接判断新旧props
对象是否全等。state
和context
的变化:是校验Fiber.lanes
是否包含renderLanes
。
校验不通过,则该节点进入正常的更新流程。
校验通过,则会进入bailoutOnAlreadyFinishedWork
方法,进一步判断满足的优化程度:
- 高度优化:跳过整个子树的更新流程【
beginWork
和completeWork
】。 - 一般优化:通过节点自身的更新流程,也就是直接克隆生成下一层子节点。
4,一个Fiber
节点在更新时,执行正常的更新逻辑和进入Bailout
策略逻辑最大的不同在于创建子节点的方式:
Bailout
策略:调用cloneChildFibers
方法,克隆旧节点生成新的Fiber
节点,重点是保留了原props
。- 正常更新逻辑:调用
reconcileChildren
方法,复用旧节点生成新的Fiber
节点,重点是使用了新建的react元素对象的props
。
5,一个组件在执行正常的更新逻辑时,会重新生成组件内所有子节点的react元素对象,然后调用reconcileChildren
方法,根据react元素对象创建对应的Fiber
节点,即子组件节点的pendingProps
属性存储的是新的props
对象,所以子节点在执行它的beginWork
时无法通过Bailout
策略校验,只能被动的重新渲染了。
6,一个组件在执行正常的更新逻辑时,在计算出最新的state
之后,在调用reconcileChildren
方法之前,其实中间还有一次Bailout
策略校验,也就是说组件类型的Fiber
前后会执行两次Bailout
策略校验,但是后面这一次也就是组件内部的这次Bailout
策略校验条件比较苛刻,实际项目中难以满足。
7,下章我们会讲解React.memo
方法,它的源码是包裹我们传入的普通函数组件,在react内部生成一个新的memo
组件节点,它的优化原理就是在beginWork
之后,组件更新之前,再执行一次Bailout
策略校验。
结束语
以上就是react普通组件更新流程的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!