从源码角度看React中的memo

最近决定看一下react源码中关于memo,useMemo,useCallback,目前看到了memo部分就准备写一篇文章,后续应该会继续更新。

先来看memeo的源码吧:

memo源码

以下是忽略 __DEV__ 后的简化版 memo


js

代码解读

复制代码

export function memo<Props>( type: React$ElementType, compare?: (oldProps: Props, newProps: Props) => boolean, ) { const elementType = { $$typeof: REACT_MEMO_TYPE, type, compare: compare === undefined ? null : compare, }; return elementType; }

其中:

  • type:一个有效的React组件
  • compare:可选的自定义比较函数,用于判断新旧 props 是否相等。如果未提供,则使用默认的浅比较

可以看出,memo源码中,创建了一个elementType对象,其中

  • 设置 $$typeof: REACT_MEMO_TYPE,可以使得react将其与普通对象相区别,从而快速判断这个组件是一个memo包装的组件
  • type:原始的组件(即传入的 type 参数)。
  • compare:自定义的比较函数(如果未提供,则为 null)。

memo的作用

  • 避免不必要的重新渲染:当组件的 props 没有发生变化时,跳过组件的重新渲染
  • 通过比较新旧 props 来决定是否需要更新:默认情况下,React.memo 使用浅比较来判断 props 是否发生变化。开发者也可以提供自定义的比较函数。

浅比较逻辑

那么问题来了,memo源码中并没有浅比较的代码,他是怎么实现浅比较的呢?

因为浅比较的逻辑是由 React 的渲染器(Reconciler)在组件更新时处理的,为不是在memo中实现

这种设计的好处是分离关注点,使代码更加模块化和可维护。

而浅比较的代码是下面这样的:


js

代码解读

复制代码

/** * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */ //执行通过遍历对象的键并返回false,当任何键的值在参数之间不是严格相等时。 //当所有键的值都严格相等时返回true。 function shallowEqual(objA: mixed, objB: mixed): boolean { //如果两个值完全相等(包括 NaN 和 -0 的特殊情况),直接返回 true if (Object.is(objA, objB)) {//基础数据类型直接比较值,引用数据类型比较引用地址 return true; } //如果任意一个不是对象,或者为 null,则返回 false if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } //获取两个对象的键,并比较键的数量 const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } //遍历键,检查每个键是否存在于另一个对象,并且对应的值是否相等 for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || // 检查键是否存在 !Object.is(objA[keysA[i]], objB[keysA[i]]) // 比较值是否相等 ) { return false; } } // 如果所有检查都通过,则返回 true return true; }

逐步解析
  1. 使用 Object.is 判断是否完全相等

    • 如果两个值完全相等(包括 NaN-0 的特殊情况),直接返回 true
    • 这一步可以快速排除大部分简单的情况。
  2. 检查类型和 null

    • 如果任意一个值不是对象(例如是原始值),或者为 null,则直接返回 false
    • 因为只有对象才需要进一步比较键和值。
  3. 比较键的数量

    • 如果两个对象的键数量不同,则它们不可能相等,直接返回 false
  4. 遍历键并比较值

    • 遍历对象 objA 的所有键,检查:

      • 该键是否也存在于 objB 中(通过 hasOwnProperty 检查)。
      • 两个对象在该键上的值是否相等(通过 Object.is 比较)。
    • 如果有任何一个键不满足条件,则返回 false

  5. 返回结果

    • 如果所有键和值都匹配,则返回 true

至此就完成了浅比较

那么有了一个疑问,浅比较函数是在哪调用的呢?

再看源码

packages/react-reconciler/src/ReactFiberBeginWork.js文件中有一个updateMemoComponent方法,他是实现memeo的关键,然后以下是memo的主要部分


js

代码解读

复制代码

const currentChild = ((current.child: any): Fiber); // This is always exactly one child //判断当前 Fiber 节点是否存在需要处理的更新(Update)或上下文(Context)变更 //都完成了返回true,否则返回false const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( current, renderLanes, ); if (!hasScheduledUpdateOrContext) { // This will be the props with resolved defaultProps, // unlike current.memoizedProps which will be the unresolved ones. // nextProps 将会是已经应用了 defaultProps 的 props, // 而 current.memoizedProps 保存的是未应用 defaultProps 的原始 props。 // 获取已解析的旧 props(包含 defaultProps 处理后的结果) const prevProps = currentChild.memoizedProps; // Default to shallow comparison // 使用自定义比较函数或默认的浅比较 let compare = Component.compare; compare = compare !== null ? compare : shallowEqual; // 比较 props 和 ref if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { // 跳过子组件更新 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } // React DevTools reads this flag. workInProgress.flags |= PerformedWork; // 创建新的子 Fiber const newChild = createWorkInProgress(currentChild, nextProps); newChild.ref = workInProgress.ref; newChild.return = workInProgress; workInProgress.child = newChild; return newChild; }

updateMemoComponent函数首先判断当前的fiber节点是否存在需要处理的更新(组件自身调用setState等,不包括父组件传递props变化)或者上下文变更(仅检测通过 useContext 订阅的 Context 值的变化)

如果都完成了(无需更改),就进入if,看有没有自定义比较函数,没有就使用浅比较,比较新旧组件compare(prevProps, nextProps),是同一个就调用 bailoutOnAlreadyFinishedWork 函数来阻止组件重新渲染跳过子组件更新(跳过渲染)

如果没有完成(需要更改)就创建新的fiber节点

bailoutOnAlreadyFinishedWork 的核心逻辑如下:


js

代码解读

复制代码

function bailoutOnAlreadyFinishedWork( current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes, ): Fiber | null { if (current !== null) { //如果当前组件可以跳过重新渲染,则直接复用之前的依赖(如上下文依赖)。 workInProgress.dependencies = current.dependencies; } //标记该组件跳过的更新优先级(lanes) markSkippedUpdateLanes(workInProgress.lanes); //检查子节点是否也有未完成的工作。 if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // 子节点没有需要处理的更新时直接返回 null if (current !== null) { //当父组件跳过更新时,检查并传播可能遗漏的上下文变化到子组件 lazilyPropagateParentContextChanges(current, workInProgress, renderLanes); if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { return null; } } else { return null; } } //当子树需要更新时,克隆子 Fiber 节点。 cloneChildFibers(current, workInProgress); return workInProgress.child; }

原文:https://juejin.cn/post/7477529979523268671

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值