最近决定看一下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; }
逐步解析
-
使用
Object.is
判断是否完全相等- 如果两个值完全相等(包括
NaN
和-0
的特殊情况),直接返回true
。 - 这一步可以快速排除大部分简单的情况。
- 如果两个值完全相等(包括
-
检查类型和 null
- 如果任意一个值不是对象(例如是原始值),或者为
null
,则直接返回false
。 - 因为只有对象才需要进一步比较键和值。
- 如果任意一个值不是对象(例如是原始值),或者为
-
比较键的数量
- 如果两个对象的键数量不同,则它们不可能相等,直接返回
false
。
- 如果两个对象的键数量不同,则它们不可能相等,直接返回
-
遍历键并比较值
-
遍历对象
objA
的所有键,检查:- 该键是否也存在于
objB
中(通过hasOwnProperty
检查)。 - 两个对象在该键上的值是否相等(通过
Object.is
比较)。
- 该键是否也存在于
-
如果有任何一个键不满足条件,则返回
false
。
-
-
返回结果
- 如果所有键和值都匹配,则返回
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