从React Diff算法看如何写出高性能的组件
发布时间: 2025-10-07
前言
React的Diff算法是React的核心,它通过对比找出虚拟DOM树的差异,最小化实际DOM操作的次数, 从而提高渲染效率。因此很多高级面试时会问Vue或者React的diff算法的,请注意这并不是背八股文,而是可以融入到工作实践中 来提高组件的性能、避免低效代码的必备知识。
今天我从React的diff算法细节以及如何融入到实践中两个方向来聊聊,如果有空的话,后面我还会去把Vue3的源码翻出来看看。
讲Diff之前
在讲Diff算法之前,我们需要先了解一下为什么会需要Diff:React的JSX到Dom渲染、提交、绘制真实DOM到浏览器的过程, 大家都至少也听说过虚拟DOM的概念,不管是Vue还是React,它们都是基于虚拟DOM的,本质上就是因为这种MVVM框架基于数据驱动的思想, 任何的数据变动都可能需要HTML做出相应的变化,但是js操作真实DOM的成本非常昂贵,因此MVVM框架也就发明了一套虚拟DOM的技术,你的js代码中的数据 变化和dom操作都是操作的内存中的虚拟DOM,等到一轮数据变化完成之后,React会根据Diff算法找出虚拟DOM树的差异,最小化实际DOM操作的次数, 从而提高渲染效率。
Diff算法会对每个节点进行打flag,标记出它是要删除的、新增的、修改的等等,然后react-dom会根据这些标记来同步Dom。
当然这个过程也不是把整个虚拟DOM全部完整的重绘一次,而是只重绘有差异的部分,这就是Diff算法的优势所在。 现在我们基于最新的React19的源码 来看看这个Diff算法的核心代码,然后是对我们实际开发带来哪些指导。
React的Diff算法
基础知识及名词解释
讲Diff算法之前,要讲2个名词及概念,便于理解Diff算法的核心原理。
- 深度优先
- 链表
- Fiber(Fiber树)
- 双缓存机制
深度优先
指的是比对dom的过程,是从两棵树的根节点开始,深度优先,而非广度优先,这个不理解可以去查查算法基础知识。
链表
链表是一种链式的数据结构,每个节点包含了数据本身和指向下一个节点的指针。
链表形如:
A:{data:'this is data A',sibling:B},
B:{data:'this is data B',sibling:C},
C:{data:'this is data C'}每个数据都有一个指针指向下一个数据,上方的sibling就是表示当前的下一个节点是谁,如果没有就表示当且链结束了。 它的优势就是可以在任意位置添加删除节点,只需要改变指针的指向即可,时间复杂度为O(1)。
Fiber树
Fiber树是React16引入的一种新的架构,它的核心思想就是将虚拟DOM树转换为Fiber树, 每个Fiber节点就是一个链表的节点,看起来是一个二维的链表,实际上对应的是一个完整的树结构, 每个节点包含了当前节点的信息以及指向下一个节点的指针、父节点指针、第一个子节点指针, 最终形成了一个完整的树结构。
我们来看看在Fiber节点怎么表示的属性:
{
key:'',// 节点的key,用于唯一标识节点,没指定的话就是索引
type:'',// 节点的类型,例如div、function、class等
return:null,// 指向父节点的指针,你没看错他的名字就叫return不叫parent,只是个命名而已
sibling:null,// 指向兄弟节点的指针,都是同一层级的
child:null,// 指向第一个子节点的指针,child节点可能有多个但是他指向第一个节点
alternate:null,// 指向另一个Fiber节点的指针,用于双缓存机制
index:0,// 节点在父节点中的索引位置
flags:0,// 节点的标记,用于表示节点的状态,例如是否需要更新等
}单独说明一下flags的定义,有很多在(packages/react-reconciler/src/ReactFiberFlags.js)中,本文只会涉及到两种:
- Placement 表示需要在真实DOM中插入节点
- ChildDeletion 表示需要删除真实DOM中的节点
实际上Fiber的属性还有一些,我把完整详细的放到本文最后,有兴趣可以看看,也可以自己去看源码:ReactFiber 。
现在我来给一个完整的fiber的示例:
<div id="root">
<head key="head"></head>
<body key="body" style="margin:0;padding:0">
<nav key="nav" class="header-nav"></nav>
<div id="container" key="container" class="content-wrap" style="display:flex">
<aside id="sidebar" key="sidebar" class="side-bar" style="width:200px"></aside>
<main id="main" key="main" class="main-content" style="flex:1"></main>
</div>
</body>
</div>得到它的Fiber树数据如下:
根Fiber(root) 数据:{return:null,sibling:null,child:head}
└── head (页面头部) 数据:{return:root,sibling:body,child:null}
└── body (页面主体,核心) 数据:{return:root,sibling:null,child:nav}
├── nav (顶部导航栏,一级子节点) 数据:{return:body,sibling:container,child:null}
└── div#container (内容容器) 数据:{return:body,sibling:null,child:sidebar}
├── aside#sidebar (左侧侧边栏,二级子节点) 数据:{return:container,sibling:main,child:null}
└── main#main (右侧主内容区,二级子节点) 数据:{return:container,sibling:null,child:null}事实上虚拟DOM就是一堆Fiber节点对象维护在内存中的,当页面变化的时候也就是,会用新的虚拟DOM树通过alternate指针去和旧的节点进行比较。
双缓存机制
React的双缓存机制和显卡的双缓存机制一致,就是在内存中维护2棵Fiber树,一颗是旧的Fiber树,一颗是新的Fiber树, 其中这两棵树一般分为Current(当前Dom同步的树,正式且不可修改的树)和WorkInProgress(执行修改的Dom树,草稿树),他们中的每个节点都有alternate指针, 指向对方的相同节点:
比如Current中的Body.alternate=WIP的Body.alternate, 反过来也是成立的:WIP的Body.alternate=Current中的Body.alternate
通常我们的改动会发生在WIP树上,等到操作完了之后,就地转换,WIP树直接作为Current,变成正式Fiber树且不允许修改, 之前的Current变为WIP树。然后如果再次变化,他们的树的定义又会再次交换,始终是在这两颗树上来回指定一个作为Current,一个作为WIP, 这就是双缓冲机制,他的好处在于内存复用不用重复new Fiber节点出来。还有就是便于Diff的时候判断位置变化,后面看源码。
我们都知道DOM是树结构,树结构的典型特征是每个节点只有一个父节点,但是可以有多个子节点。 然后我们既然是对比就有2颗树(旧DOM和新DOM),为什么只对比同层级?
Diff算法源码分析
Diff算法的核心是判断某个节点应不应该存在,因此它主要是处理节点的添加、删除、移动操作。 (更新并不在这个算法中)。Diff算法源码ReactChildFiber.js
1.首先在开始比对节点的时候入口
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}可以看到如果要对比的current节点为空,则表示是新增所以用的是mountChildFibers,只有当current节点不为空时, 才会用reconcileChildFibers去对比current.child和变化的nextChildren,看看哪些需要更新。
然后看看reconcileChildFibersImpl分发函数,我尽量写注释
function reconcileChildFibersImpl(
returnFiber: Fiber,// 要对比的节点的父节点
currentFirstChild: Fiber | null, // 第一个子节点(前面说了是链式结构,因此只需要拿到第一个)
newChild: any, // 有变化的节点,他就是jsx中的节点,可能是单个(一个div),也可能是多个(ul节点下的一堆li)
lanes: Lanes,
): Fiber | null {
const isUnkeyedUnrefedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null &&
(enableFragmentRefs ? newChild.props.ref === undefined : true);
// 展开顶层 Fragment,这里是处理被<></>包裹的情况。
if (isUnkeyedUnrefedTopLevelFragment) {
validateFragmentProps(newChild, null, returnFiber);
newChild = newChild.props.children;
}
/* 接下来分为两种情况,对象和text|int
下面会根据传入的nextChild的类型来调用不同的处理方法
为了处理不同的情况,我们会根据newChild的类型来调用不同的处理方法
这里我就挑2个(单React元素和数组)出来放到下面细说吧。
*/
// 处理对象类型
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
// React 元素
case REACT_ELEMENT_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
// 因为是链表,所以只需要拿到第一个节点返回
const firstChild = placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
// Portal
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
// Lazy 组件:解析后递归
case REACT_LAZY_TYPE: {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const result = resolveLazy((newChild: any));
const firstChild = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
result,
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
}
// 数组,数组的对比是个比较麻烦的事情
if (isArray(newChild)) {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const firstChild = reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
// 同步可迭代对象
if (getIteratorFn(newChild)) {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const firstChild = reconcileChildrenIteratable(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
// 异步可迭代对象
if (
enableAsyncIterableChildren &&
typeof newChild[ASYNC_ITERATOR] === 'function'
) {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
const firstChild = reconcileChildrenAsyncIteratable(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
// Thenable(Promise-like):解包后递归
// Usable 类型会持续解包直到得到非 Usable 类型
if (typeof newChild.then === 'function') {
const thenable: Thenable<any> = (newChild: any);
const prevDebugInfo = pushDebugInfo((thenable: any)._debugInfo);
const firstChild = reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
unwrapThenable(thenable),
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}
// Context:读取值后递归
if (newChild.$$typeof === REACT_CONTEXT_TYPE) {
const context: ReactContext<mixed> = (newChild: any);
return reconcileChildFibersImpl(
returnFiber,
currentFirstChild,
readContextDuringReconciliation(returnFiber, context, lanes),
lanes,
);
}
// 无法识别的对象类型
throwOnInvalidObjectType(returnFiber, newChild);
}
// 处理文本类型(string, number, bigint)
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number' ||
typeof newChild === 'bigint'
) {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
'' + newChild,
lanes,
),
);
}
// 其他情况(null, undefined, boolean):删除所有旧子节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}现在单独看看单元素怎么处理的:
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
/**
* 这里通俗的说一下它的判断逻辑,首先child是一个链表,通过while可以判断所有的节点,
* 拿到每一个节点去和
* 当前的新节点(只有一个)进行对比,通过对他们的key和type来判断他们是否是同一个节点,
* 把没匹配上的都标记为删除,如果都没匹配上给新节点标记为新增
*
* 【重点、重点、重点】得到结论:匹配节点的逻辑就是ReactElement的type和key去对比Fiber的type和key
* 并且对比仅限于当前同父元素的所有子节点,不对比孙辈节点,也不对比祖辈节点
*/
// 遍历所有旧子节点,寻找可复用的
while (child !== null) {
// 检查 key 是否匹配
if (
child.key === key ||
(enableOptimisticKey && child.key === REACT_OPTIMISTIC_KEY)
) {
// key 匹配,检查类型
const elementType = element.type;
// Fragment 特殊处理
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// key 和类型都匹配,复用
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props.children);
if (enableOptimisticKey) {
// If the old key was optimistic we need to now save the real one.
existing.key = key;
}
if (enableFragmentRefs) {
coerceRef(existing, element);
}
existing.return = returnFiber;
if (__DEV__) {
existing._debugOwner = element._owner;
existing._debugInfo = currentDebugInfo;
}
validateFragmentProps(element, existing, returnFiber);
return existing;
}
} else {
// 非 Fragment 元素
if (
// 类型完全相同
child.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
// 热重载兼容性
(__DEV__
? isCompatibleFamilyForHotReloading(child, element)
: false) ||
// Lazy types should reconcile their resolved type.
// We need to do this after the Hot Reloading check above,
// because hot reloading has different semantics than prod because
// it doesn't resuspend. So we can't let the call below suspend.
// Lazy 组件类型匹配
(typeof elementType === 'object' &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === child.type)
) {
// key 和类型都匹配,复用
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
if (enableOptimisticKey) {
// If the old key was optimistic we need to now save the real one.
existing.key = key;
}
coerceRef(existing, element);
existing.return = returnFiber;
if (__DEV__) {
existing._debugOwner = element._owner;
existing._debugInfo = currentDebugInfo;
}
return existing;
}
}
// key 匹配但类型不匹配,删除所有旧节点
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
// key 不匹配,删除这个旧节点,继续检查下一个
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// 没有找到可复用的节点,创建新的
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
lanes,
element.key,
);
if (enableFragmentRefs) {
coerceRef(created, element);
}
created.return = returnFiber;
if (__DEV__) {
// We treat the parent as the owner for stack purposes.
created._debugOwner = returnFiber;
created._debugTask = returnFiber._debugTask;
created._debugInfo = currentDebugInfo;
}
validateFragmentProps(element, created, returnFiber);
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
coerceRef(created, element);
created.return = returnFiber;
if (__DEV__) {
created._debugInfo = currentDebugInfo;
}
return created;
}
}单节点的diff对比逻辑结论:
匹配节点的逻辑就是ReactElement的type和key去对比Fiber的type和key,并且对比仅限于当前同父元素的所有子节点,不对比孙辈节点,也不对比祖辈节点。
这里只比对同级,可以避免2个树对比(Current的A节点去比对WIP的每一个节点),把复杂度从 O (n³) 降到 O (n)。
然后看一下一个复杂的场景,传入的newChild是一个数组的情况
if (isArray(newChild)) {
const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
// 数组的比对算法相对于比较复杂
const firstChild = reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
currentDebugInfo = prevDebugInfo;
return firstChild;
}来看看其中的reconcileChildrenArray函数
function reconcileChildrenArray(
returnFiber: Fiber,// wip树父节点
currentFirstChild: Fiber | null,//current树对应节点的第一个子节点
newChildren: Array<any>, // 要比对的变化内容,这里可以理解为是jsx数组(比如ul下的多个li)
lanes: Lanes,
): Fiber | null {
/**
* 因为现在要对两个数组进行比对,也不会使用递归,所以这里需要定义一些变量
*/
// 用于检测重复 key 的集合
let knownKeys: Set<string> | null = null;
// 结果链表的头指针
let resultingFirstChild: Fiber | null = null;
// 上一个新 Fiber,用于构建兄弟链表
let previousNewFiber: Fiber | null = null;
// 当前正在比较的旧 Fiber
let oldFiber = currentFirstChild;
// 最后一个放置的索引,用于判断是否需要移动
let lastPlacedIndex = 0;
// 新数组的当前索引
let newIdx = 0;
// 下一个旧 Fiber(用于处理空槽位情况)
let nextOldFiber = null;
/**
* 这里数组比较有2个方案,1.快速比较,直接顺序比对两个数组,假定前面都能匹配,只处理最后
* 新增、删除的情况下,这样比较快,o(n)时间复杂度,但是一旦前面有一个节点对不上,就要进入复杂比对。
*/
// ========== 第一阶段:顺序比较(快速路径)==========
// 同时遍历新旧列表,尝试在相同位置复用
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 处理旧 Fiber 的索引跳跃情况(可能由于之前的 null 子节点)
if (oldFiber.index > newIdx) {
// 旧 Fiber 的索引大于当前位置,说明当前位置之前是空的
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// 尝试在当前槽位复用,updateSlog内部也是比对key和type来判定是否同一个节点
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber === null 表示 key 不匹配,需要退出快速路径
if (newFiber === null) {
// TODO: This breaks on empty slots like null children. That's
// unfortunate because it triggers the slow path all the time. We need
// a better way to communicate whether this was a miss or null,
// boolean, undefined, etc.
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 处理类型不匹配的情况
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// 槽位匹配但没有复用旧 Fiber(类型不同),需要删除旧节点
deleteChild(returnFiber, oldFiber);
}
}
// 放置子节点,判断是否需要移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 构建结果链表
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
// 第一个节点,设置为链表头
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
// 链接到前一个节点
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
/**
* 本质是比对两个数组,如果新数组处理完,但是旧数据还有没处理的节点,
* 就表示是不需要的,应该把剩下的旧节点标记为删除
*/
// ========== 第二阶段 A:新列表遍历完成 ==========
if (newIdx === newChildren.length) {
// 新列表已处理完,删除剩余的旧节点
deleteRemainingChildren(returnFiber, oldFiber);
// Hydration 相关处理
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
/**
* 如果新数组还有没处理的数据,而旧数组都处理了,则表示新数组中的这些数据都是新增的
*/
// ========== 第二阶段 B:旧列表遍历完成 ==========
if (oldFiber === null) {
// 旧列表已处理完,剩余的新节点都是插入
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}
/**
* 如果逻辑走到这里,说明新数组没处理完、旧数组也没处理完,那就是快速路径处理失败了,
* 就是前面说的,如果旧数组的中间或前面部分有变化(增删、调整顺序),也就是除了在旧数组
* 的最后删除或新增以外的情况,都会走到这里来。
* 这里就需要先对旧数据做Map映射,方便查找某个节点是否还在了,确认删除、新增的节点。
*
* 核心是遍历旧数组,把他们放入Map中,使用他们的key||index作为Map的key,然后检查newChild的key是否
* 在旧数组中(还有type的检查),在的话就表示是可复用的节点,否则就是新增的节点,最后把Map(旧数组)中剩余的节点标记删除。
*
*/
// ========== 第三阶段:Map 查找(慢路径)==========
// 将剩余旧节点放入 Map,为了可以支持 O(1) 查找
const existingChildren = mapRemainingChildren(oldFiber);
// 继续处理剩余的新节点,使用 Map 查找可复用的旧节点
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (__DEV__) {
knownKeys = warnOnInvalidKey(
returnFiber,
newFiber,
newChildren[newIdx],
knownKeys,
);
}
if (shouldTrackSideEffects) {
const currentFiber = newFiber.alternate;
if (currentFiber !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
// 复用了旧 Fiber,从 Map 中删除,避免后续被误删
if (
enableOptimisticKey &&
currentFiber.key === REACT_OPTIMISTIC_KEY
) {
existingChildren.delete(-newIdx - 1);
} else {
existingChildren.delete(
currentFiber.key === null ? newIdx : currentFiber.key,
);
}
}
}
// 放置子节点
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 构建结果链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除 Map 中剩余的旧节点(它们在新列表中不存在)
if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// Hydration 相关处理
if (getIsHydrating()) {
const numberOfForks = newIdx;
pushTreeFork(returnFiber, numberOfForks);
}
return resultingFirstChild;
}通过对这两段源码的分析,最终可以得到结论:
- 比对是否复用节点比对的是type和key
- 比对只在同级元素中比对,不会跨级(时间复杂度降至O(n))
以上代码仅标记这些节点是否该不该存在(删除、新增),但是有没有更新,则是另外的逻辑在控制.
组件性能优化
核心原则
根据上面的两条结论,我们可以得到在React开发组件时应该避免的问题:
- 只对比同层级,所以我们应该避免跨层级操作
- type类型不同则重建,应该变节点类型频繁切换
- 列表的key应该稳定,key变化也会导致重建
Bad Case分析
- 没有key的列表渲染 会导致频繁的删除重建,比如在列头插入一条数据,根据上面源码会认为第一条数据对不上 后面也都是对不上的,直接全删。
<ul>
{list.map(item => (
<li>{item.name}</li>
))}
</ul>- 使用index做key或者使用随机key index是下标,比如你翻转数组,还是会被重建(删除第一条,后面所有的key都会自动减1,和current的key就对不上会 认为是被删除,导致重建),并且还会导致状态错位的问题(做动态增加输入框的场景会出现),使用随机key类同。
<ul>
{list.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
<ul>
{list.map(item => (
<li key={Math.random()}>{item.name}</li>
))}
</ul>- type变化,会导致重建
{isShow ? <div key="box">内容</div> : <p key="box">内容</p>}
{isList ? <List /> : <Card />}只修改prop
<div style={{display: isShow ? 'block' : 'none'}}>内容</div>
// 组件不销毁,只切换显示,但是会都加载,对内存有挑战,这里需要取舍
<>
<List style={{ display: isList ? 'block' : 'none' }} />
<Card style={{ display: isList ? 'none' : 'block' }} />
</>说到重建顺便提两个组件重建比较容易忽略的情况:
- 子组件会频繁重建 user和handleClick是在每次渲染都会创建一次新的引用,并且父组件重新渲染都会触发子组件的重新渲染
function Parent() {
const [count, setCount] = useState(0);
const user = { name: '张三', age: 20 };
const handleClick = () => console.log(count);
return (
<div>
<Child user={user} onClick={handleClick} />
</div>
);
}
function Child({ user, onClick }) {
return <div>{user.name}</div>;
}
// 改为:
function Parent() {
const [count, setCount] = useState(0);
// 缓存对象引用
const user = useMemo(() => ({ name: '张三', age: 20 }), []);
// 缓存函数引用
const handleClick = useCallback(() => console.log(count), [count]);
return (
<div>
<Child user={user} onClick={handleClick} />
</div>
);
}
// 使用 memo 包裹,只有 props 引用变化时才重渲染,
// 而user和handleClick使用了useMemo和useCallback已经稳定下
// 来不会每次渲染都重新创建新引用
const Child = React.memo(function Child({ user, onClick }) {
return <div>{user.name}</div>;
});- 直接定义函数/对象
// 直接在jsx中定义函数/对象,每次渲染都相当于一个新的引用
<div onClick={() => setCount(count + 1)}>点击</div>
<Child config={{ page: 1, size: 10 }} />
// 改为:
// 用 useCallback 缓存
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// 用 useMemo 缓存
const config = useMemo(() => ({ page: 1, size: 10 }), []);
return (
<div>
<div onClick={handleClick}>点击</div>
<Child config={config} />
</div>
);Fiber属性大全
export type Fiber = {
// ============================================================================
// 第一部分:节点标识属性
// 这些属性用于标识 Fiber 节点的类型和身份
// ============================================================================
/**
* tag - 节点类型标识
*
* 标识这个 Fiber 是什么类型的组件,决定了如何处理这个节点。
*
* 常见的 tag 值(定义在 ReactWorkTags.js):
* - FunctionComponent = 0 函数组件
* - ClassComponent = 1 类组件
* - HostRoot = 3 根节点(ReactDOM.render 的容器)
* - HostComponent = 5 原生 DOM 元素(div, span 等)
* - HostText = 6 文本节点
* - Fragment = 7 Fragment
* - ContextProvider = 10 Context.Provider
* - ContextConsumer = 11 Context.Consumer
* - MemoComponent = 14 React.memo 包裹的组件
* - LazyComponent = 16 React.lazy 懒加载组件
* - SuspenseComponent = 13 Suspense 组件
*
* 示例:
* <div> → tag = HostComponent (5)
* <App /> (函数组件) → tag = FunctionComponent (0)
* "hello" → tag = HostText (6)
*/
tag: WorkTag,
/**
* key - 节点唯一标识
*
* 用于 Diff 算法中识别节点身份,帮助 React 判断节点是否可以复用。
*
* 重要性:
* - 在列表渲染中,key 帮助 React 识别哪些元素改变了、添加了或删除了
* - 相同 key + 相同 type = 可复用
* - key 不同 = 销毁旧节点,创建新节点
*
* 示例:
* <li key="a">A</li> → key = "a"
* <li>B</li> → key = null(没有显式指定)
*
* 注意:
* - 不要用数组索引作为 key(除非列表是静态的)
* - key 只需要在兄弟节点中唯一,不需要全局唯一
*/
key: ReactKey,
/**
* elementType - 元素类型(原始值)
*
* 保存 element.type 的原始值,用于协调时保持身份。
*
* 与 type 的区别:
* - elementType 保存原始值
* - type 是解析后的值(比如 lazy 组件解析后的实际组件)
*
* 示例:
* <div> → elementType = 'div'
* <App /> → elementType = App (函数引用)
* React.lazy(() => import('./Lazy')) → elementType = LazyComponent 对象
*/
elementType: any,
/**
* type - 组件类型(解析后)
*
* 存储组件的函数/类/标签名。
*
* 不同类型的值:
* - 原生元素:字符串,如 'div', 'span'
* - 函数组件:函数引用
* - 类组件:类引用
* - Fragment:Symbol(react.fragment)
*
* 示例:
* <div> → type = 'div'
* <App /> → type = function App() {...}
* <MyClass /> → type = class MyClass {...}
*/
type: any,
/**
* stateNode - 关联的实际节点
*
* 指向 Fiber 对应的实际实例,不同类型的 Fiber 指向不同的东西。
*
* 不同类型的 stateNode:
* - HostComponent (div等):真实 DOM 节点
* - ClassComponent:类组件实例 (this)
* - HostRoot:FiberRoot 对象
* - FunctionComponent:null(函数组件没有实例)
* - HostText:文本 DOM 节点
*
* 示例:
* <div id="app"> → stateNode = document.getElementById('app')
* <MyClass /> → stateNode = new MyClass() 实例
*/
stateNode: any,
// ============================================================================
// 第二部分:树结构属性
// Fiber 树使用链表结构,而不是传统的树结构
// 这样设计是为了方便遍历和中断恢复
// ============================================================================
/**
* return - 父 Fiber
*
* 指向父节点,类似于函数调用栈的返回地址。
* 当处理完当前节点后,会返回到 return 指向的节点继续处理。
*
* 命名为 "return" 而不是 "parent" 的原因:
* - 概念上类似于函数调用栈的返回地址
* - 处理完子节点后要"返回"到父节点
*
* 示例:
* App
* │
* └── Header (Header.return = App)
* │
* └── Logo (Logo.return = Header)
*/
return: Fiber | null,
/**
* child - 第一个子 Fiber
*
* 指向第一个子节点。只指向第一个,其他子节点通过 sibling 连接。
*
* 示例:
* Parent
* │ child
* ▼
* Child1 ──sibling──► Child2 ──sibling──► Child3
*
* Parent.child = Child1
* Child1.sibling = Child2
* Child2.sibling = Child3
*/
child: Fiber | null,
/**
* sibling - 下一个兄弟 Fiber
*
* 指向下一个兄弟节点,形成单向链表。
*
* 为什么用链表而不是数组?
* - 方便插入和删除操作
* - 遍历时可以随时中断和恢复
* - 不需要维护索引
*/
sibling: Fiber | null,
/**
* index - 在兄弟节点中的索引
*
* 表示当前节点在父节点的所有子节点中的位置(从 0 开始)。
*
* 用途:
* - Diff 算法中判断节点是否需要移动(lastPlacedIndex 算法)
* - 帮助确定 DOM 操作的位置
*
* 示例:
* <ul>
* <li>A</li> → index = 0
* <li>B</li> → index = 1
* <li>C</li> → index = 2
* </ul>
*/
index: number,
// ============================================================================
// 第三部分:Ref 相关属性
// ============================================================================
/**
* ref - 引用
*
* 存储组件的 ref,可以是:
* - null:没有 ref
* - 回调函数:(instance) => void
* - RefObject:{ current: instance }(useRef 创建的)
*
* 示例:
* <div ref={myRef}> → ref = myRef (RefObject)
* <div ref={(el) => this.div = el}> → ref = 回调函数
*/
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
/**
* refCleanup - ref 清理函数
*
* 当 ref 是回调函数且返回了清理函数时,存储在这里。
* 在组件卸载或 ref 变化时调用。
*
* React 18+ 支持 ref 回调返回清理函数:
* <div ref={(el) => {
* // 设置
* return () => {
* // 清理
* };
* }} />
*/
refCleanup: null | (() => void),
// ============================================================================
// 第四部分:Props 和 State 属性
// 这些是组件的核心数据
// ============================================================================
/**
* pendingProps - 新的 props(待处理)
*
* 本次更新传入的新 props,还未被处理。
* 在 beginWork 开始时使用,处理完成后会变成 memoizedProps。
*
* 生命周期:
* 1. 父组件渲染,传入新 props
* 2. pendingProps = 新 props
* 3. beginWork 处理
* 4. completeWork 后:memoizedProps = pendingProps
*/
pendingProps: any, // This type will be more specific once we overload the tag.
/**
* memoizedProps - 上次渲染的 props
*
* 上一次渲染使用的 props,用于比较是否有变化。
*
* 用途:
* - 判断是否需要更新:oldProps !== newProps
* - bailout 优化:如果 props 没变,可能跳过更新
*/
memoizedProps: any, // The props used to create the output.
/**
* updateQueue - 更新队列
*
* 存储待处理的更新,不同类型的组件有不同的结构:
*
* 类组件:
* {
* shared: { pending: Update 环形链表 },
* effects: 副作用列表
* }
*
* 函数组件:
* 存储 useEffect 等副作用
*
* HostRoot:
* 存储 ReactDOM.render 的更新
*/
updateQueue: mixed,
/**
* memoizedState - 上次渲染的 state
*
* 存储组件的状态,不同类型组件的结构不同:
*
* 类组件:
* - 就是 this.state 对象
*
* 函数组件:
* - Hooks 链表的头节点
* - 每个 Hook 是链表中的一个节点
*
* 示例(函数组件的 Hooks 链表):
* function App() {
* const [count, setCount] = useState(0); // Hook 1
* const [name, setName] = useState(''); // Hook 2
* useEffect(() => {...}, []); // Hook 3
* }
*
* memoizedState 结构:
* Hook1 { memoizedState: 0, next: Hook2 }
* └──► Hook2 { memoizedState: '', next: Hook3 }
* └──► Hook3 { memoizedState: effect, next: null }
*/
memoizedState: any,
/**
* dependencies - 依赖项
*
* 存储组件的 Context 依赖,用于判断 Context 变化时是否需要更新。
*
* 结构:
* {
* lanes: 优先级,
* firstContext: ContextDependency 链表
* }
*
* 当 Context 值变化时,React 会遍历依赖这个 Context 的 Fiber,
* 标记它们需要更新。
*/
dependencies: Dependencies | null,
// ============================================================================
// 第五部分:模式和副作用
// ============================================================================
/**
* mode - 工作模式
*
* 位掩码,描述 Fiber 及其子树的工作模式。
* 创建时从父节点继承,之后不应改变。
*
* 常见模式(定义在 ReactTypeOfMode.js):
* - NoMode = 0b0000 无特殊模式
* - ConcurrentMode = 0b0001 并发模式
* - StrictMode = 0b0010 严格模式(开发时双重渲染检查)
* - ProfileMode = 0b0100 性能分析模式
*
* 示例:
* <StrictMode>
* <App /> // App 及其子树都会继承 StrictMode
* </StrictMode>
*/
mode: TypeOfMode,
/**
* flags - 副作用标记
*
* 位掩码,标记这个 Fiber 需要执行的副作用操作。
*
* 常见的 flags(定义在 ReactFiberFlags.js):
* - NoFlags = 0 无副作用
* - Placement = 0b0000000010 需要插入 DOM(新增或移动)
* - Update = 0b0000000100 需要更新 DOM 属性
* - ChildDeletion = 0b0000010000 需要删除子节点
* - Ref = 0b0100000000 需要处理 ref
* - Passive = 0b0000100000000000 有 useEffect 副作用
*
* 在 commit 阶段,React 会根据 flags 执行相应的 DOM 操作。
*
* 示例:
* - 新增节点:flags |= Placement
* - props 变化:flags |= Update
* - 节点被删除:父节点 flags |= ChildDeletion
*/
flags: Flags,
/**
* subtreeFlags - 子树副作用标记
*
* 子树中所有 Fiber 的 flags 的并集。
* 用于快速判断子树是否有副作用需要处理。
*
* 优化作用:
* - 如果 subtreeFlags === NoFlags,可以跳过整个子树
* - 避免不必要的遍历
*
* 在 completeWork 阶段向上冒泡:
* parent.subtreeFlags |= child.flags | child.subtreeFlags
*/
subtreeFlags: Flags,
/**
* deletions - 待删除的子节点
*
* 存储需要删除的子 Fiber 数组。
*
* 为什么单独存储?
* - 被删除的节点不在新的 Fiber 树中
* - 但 commit 阶段需要执行它们的清理工作(卸载、ref 清理等)
*
* 示例:
* 旧:<div><A/><B/><C/></div>
* 新:<div><A/><C/></div>
*
* div.deletions = [B 的 Fiber]
*/
deletions: Array<Fiber> | null,
// ============================================================================
// 第六部分:优先级相关
// React 的并发特性依赖优先级调度
// ============================================================================
/**
* lanes - 本节点的优先级
*
* 位掩码,表示这个 Fiber 上待处理更新的优先级。
*
* Lane 模型(定义在 ReactFiberLane.js):
* - SyncLane = 0b0000000000000000000000000000010 同步优先级(最高)
* - InputContinuousLane = 0b0000000000000000000000000001000 连续输入
* - DefaultLane = 0b0000000000000000000000000100000 默认优先级
* - IdleLane = 0b0010000000000000000000000000000 空闲优先级(最低)
*
* 用途:
* - 决定更新的处理顺序
* - 高优先级更新可以打断低优先级
*/
lanes: Lanes,
/**
* childLanes - 子树的优先级
*
* 子树中所有待处理更新的优先级并集。
* 用于快速判断子树是否有指定优先级的更新。
*
* 优化作用:
* - 如果 childLanes 不包含当前处理的优先级,可以跳过子树
* - 避免不必要的遍历
*/
childLanes: Lanes,
// ============================================================================
// 第七部分:双缓冲机制
// React 同时维护两棵 Fiber 树
// ============================================================================
/**
* alternate - 双缓冲对应节点
*
* 指向另一棵树中对应的 Fiber 节点。
*
* 双缓冲机制:
* ┌─────────────────┐ ┌─────────────────┐
* │ current 树 │◄──────►│ workInProgress 树│
* │ (当前显示的) │ alternate│ (正在构建的) │
* └─────────────────┘ └─────────────────┘
*
* current.alternate = workInProgress
* workInProgress.alternate = current
*
* 作用:
* 1. Diff 对比 - 通过 alternate 找到旧节点进行比较
* 2. 复用节点 - 更新时复用 alternate 节点,减少内存分配
* 3. 状态保留 - 从 alternate 复制 memoizedState
* 4. 快速切换 - 渲染完成后交换 current 指针
*
* 生命周期:
* - 首次渲染:alternate = null(没有旧树)
* - 更新时:alternate 指向 current 树的对应节点
* - 提交后:workInProgress 变成新的 current
*/
alternate: Fiber | null,
// ============================================================================
// 第八部分:Profiler 性能分析属性
// 仅在 enableProfilerTimer 开启时使用
// ============================================================================
/**
* actualDuration - 实际渲染耗时
*
* 本次渲染这个 Fiber 及其子树花费的总时间(毫秒)。
*
* 特点:
* - 每次渲染重置为 0
* - 只有实际执行了渲染才会更新(bailout 时不更新)
* - 包含子树的时间
*
* 用于 React DevTools Profiler 显示组件渲染耗时。
*/
actualDuration?: number,
/**
* actualStartTime - 渲染开始时间
*
* 本次渲染开始的时间戳。
* 用于计算渲染耗时。
*/
actualStartTime?: number,
/**
* selfBaseDuration - 自身基础耗时
*
* 这个 Fiber 自身(不包含子树)最近一次渲染的耗时。
* 即使 bailout 也不会重置,保留上次的值。
*
* 用于估算组件的"基准"渲染成本。
*/
selfBaseDuration?: number,
/**
* treeBaseDuration - 子树基础耗时
*
* 这个 Fiber 及其所有子孙节点的 selfBaseDuration 之和。
* 在 completeWork 阶段向上累加。
*
* 用于估算整个子树的渲染成本。
*/
treeBaseDuration?: number,
// ============================================================================
// 第九部分:开发调试属性(仅 __DEV__ 模式)
// 这些属性帮助开发者调试,生产环境不存在
// ============================================================================
/**
* _debugInfo - 调试信息
*
* 存储组件的调试信息,如 Server Components 的信息。
*/
_debugInfo?: ReactDebugInfo | null,
/**
* _debugOwner - 创建者
*
* 指向创建这个 Fiber 的组件。
* 用于在错误信息中显示组件层级。
*
* 示例错误信息:
* "Warning: Each child in a list should have a unique key prop.
* Check the render method of `ParentComponent`."
* ↑ 来自 _debugOwner
*/
_debugOwner?: ReactComponentInfo | Fiber | null,
/**
* _debugStack - 调用栈
*
* 创建这个 Fiber 时的调用栈,用于错误追踪。
*/
_debugStack?: Error | null,
/**
* _debugTask - 控制台任务
*
* 用于 Chrome DevTools 的异步任务追踪。
*/
_debugTask?: ConsoleTask | null,
/**
* _debugNeedsRemount - 是否需要重新挂载
*
* 热重载时使用,标记组件是否需要完全重新挂载。
*/
_debugNeedsRemount?: boolean,
/**
* _debugHookTypes - Hooks 类型列表
*
* 记录函数组件中使用的 Hooks 类型和顺序。
* 用于检测 Hooks 调用顺序是否在渲染间保持一致。
*
* 示例:
* function App() {
* useState(); // _debugHookTypes[0] = 'useState'
* useEffect(); // _debugHookTypes[1] = 'useEffect'
* useMemo(); // _debugHookTypes[2] = 'useMemo'
* }
*
* 如果下次渲染顺序变了,React 会警告:
* "React has detected a change in the order of Hooks"
*/
_debugHookTypes?: Array<HookType> | null,
};
