Before you memo() 的原理
本文增加了对 https://overreacted.io/before you memo/ 的源码解释 原文总结 原始代码中 B 组件有重操作,在更新 A 组件时,导致了 B 的重新渲染,从而导致了页面卡顿。 在 before you memo 这篇文章中,作者提供了几个解决没有耦合的组件之间如何避免重复渲染的方法。 1. memo。使用 memo 包装组件,使组件进行浅比较,由于 A, B 组件之间无耦合关系,所以 B 的 Prop...
本文增加了对 https://overreacted.io/before you memo/ 的源码解释 原文总结 原始代码中 B 组件有重操作,在更新 A 组件时,导致了 B 的重新渲染,从而导致了页面卡顿。 在 before you memo 这篇文章中,作者提供了几个解决没有耦合的组件之间如何避免重复渲染的方法。 1. memo。使用 memo 包装组件,使组件进行浅比较,由于 A, B 组件之间无耦合关系,所以 B 的 Prop...
本文增加了对 https://overreacted.io/before-you-memo/ 的源码解释
原始代码中 B 组件有重操作,在更新 A 组件时,导致了 B 的重新渲染,从而导致了页面卡顿。
在 before you memo 这篇文章中,作者提供了几个解决没有耦合的组件之间如何避免重复渲染的方法。
React v18.2.0
在 React 的 reconciliation 阶段,beginWork 是调度节点更新的入口方法。在这个方法的开始,有一个对于可直接复用上一次渲染结果的判断。代码如下:
function beginWork(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes): Fiber | null {
const updateLanes = workInProgress.lanes
if (current !== null) {
const oldProps = current.memoizedProps
const newProps = workInProgress.pendingProps
if (oldProps !== newProps || hasLegacyContextChanged()) {
didReceiveUpdate = true
} else {
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes)
if (!hasScheduledUpdateOrContext && (workInProgress.flags & DidCapture) === NoFlags) {
didReceiveUpdate = false
// 该方法进入 bailoutOnAlreadyFinishedWork 逻辑
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes)
}
}
} else {
didReceiveUpdate = false
}
// ... reconcileChildren
}
预设 bailoutOnAlreadyFinishedWork 是直接复用旧节点的的方法,可以看到进入该方法的判断条件:
当符合上述条件进入 bailout 逻辑,则不会重新调度节点,可以直接复用上一次渲染结果。明白这一点后,可以结合文章中的几种情况各种检查原因了。
每一次 rerender 对于 ExpensiveTree 来说都是调用了 React.createElement(ExpensiveTree, null))。
export function createElement(type, config, children) {
let propName
// Reserved names are extracted
const props = {}
// ...
if (config !== null) {
// ...
props[propName] = config[propName]
}
// ...
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}
config 为 null,最后生成的 props 是 {},由于 {}==={} 的判断结果是
false,所以条件1: oldProps === newProps不符合,无法进入 bailout 逻辑,会触发渲染。
memo 方法会将 $$typeof 标记为 REACT_MEMO_TYPE,在构建 fiber 过程中,会根据 $$typeof
将 tag 赋值为 MemoComponent。
在 beginWork 的条件里,其实是不符合 条件1 跳过了第一次的 bailout。
但在接下来 reconcileChildren 的阶段中,根据 workInProgress.tag
进入updateMemoComponent方法中,可以看到一个类似beginWork的过程:
function updateMemoComponent() {
if (current === null) {
// ...
return child
}
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes)
if (!hasScheduledUpdateOrContext) {
const prevProps = currentChild.memoizedProps
let compare = Component.compare
compare = compare !== null ? compare : shallowEqual
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
}
}
return newChild
}
同样检查是否有同一优先级的更新后,通过 shallowEqual
浅比较前后 props 如果没有变化则符合 bailout 条件。
浅比较:将对象的相同的 key 的值依次对比,而不是对象整个对比, 在浅比较的情况下
{} === {}返回 true
这种情况下,ColorPicker和ExpensiveTree已经分别属于两个组件了,修改 ColorPicker 不会影响到
ExpensiveTree,所以 ExpensiveTree 可以一直复用。
在 LiftContentUp 中,对于 ExpensiveTree 来说,它在 ColorPicker 中意味着是 props.children。
对于 ColorPicker 来说,它的 props.children 在 App 中 props 也没有改变。在 setState
也没有发生变化。
所以,在调度到 ExpensiveTree 也符合 bailout 的条件。
我们最后来看一下 bailout 方法的实现,可以看到该方法最后通过复制节点完成了复用的能力。
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null
}
// 复用节点
cloneChildFibers(current, workInProgress)
return workInProgress.child
}