Skip to Content
全部文章实践出真知从React Diff算法看如何写出高性能的组件

从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.首先在开始比对节点的时候入口

ReactFiberBeginWork.js
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; }

通过对这两段源码的分析,最终可以得到结论:

  1. 比对是否复用节点比对的是type和key
  2. 比对只在同级元素中比对,不会跨级(时间复杂度降至O(n))

以上代码仅标记这些节点是否该不该存在(删除、新增),但是有没有更新,则是另外的逻辑在控制.

组件性能优化

核心原则

根据上面的两条结论,我们可以得到在React开发组件时应该避免的问题:

  • 只对比同层级,所以我们应该避免跨层级操作
  • type类型不同则重建,应该变节点类型频繁切换
  • 列表的key应该稳定,key变化也会导致重建

Bad Case分析

  1. 没有key的列表渲染 会导致频繁的删除重建,比如在列头插入一条数据,根据上面源码会认为第一条数据对不上 后面也都是对不上的,直接全删。
<ul> {list.map(item => ( <li>{item.name}</li> ))} </ul>
  1. 使用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>
  1. 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' }} /> </>

说到重建顺便提两个组件重建比较容易忽略的情况:

  1. 子组件会频繁重建 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>; });
  1. 直接定义函数/对象
// 直接在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, };
最后编辑于

hi