最近决定看一下react源码中关于memo,useMemo,useCallback,目前看到了memo部分就准备写一篇文章,后续应该会继续更新。
先来看memeo的源码吧:
memo源码
以下是忽略 __DEV__ 后的简化版 memo
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中实现
这种设计的好处是分离关注点,使代码更加模块化和可维护。
而浅比较的代码是下面这样的:
/**
* 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的主要部分
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 的核心逻辑如下:
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;
}