最近跟进部门的 PC 性能优化项目,给的 SOP 是尽量使用 useCallback、useMemo、memo,我印象中 react 文档是不推荐全部使用这些 hook 的,但又不太清楚原因,因此想通过源码看下作用的原理。
另外想吐槽一下,刷到很多文章都说不要过早优化,遇到性能问题时再去优化。我想说这种话的是没遇到 ld 心血来潮搞个性能专项,随便拍了个目标,然后要你倒推哪里可以抠出性能的场景😀。你们倒是说下怎么极致地抠出性能啊😄。
首先看导出 API
// packages/react/index.js
export {
...
memo,
useCallback,
useMemo,
...
} from './src/ReactClient';
memo
// packages/react/src/ReactMemo.js
export function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
if (__DEV__) {
if (type == null) {
console.error(
'memo: The first argument must be a component. Instead ' +
'received: %s',
type === null ? 'null' : typeof type,
);
}
}
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
if (__DEV__) {
let ownName;
Object.defineProperty(elementType, 'displayName', {
enumerable: false,
configurable: true,
get: function () {
return ownName;
},
set: function (name) {
ownName = name;
// The inner component shouldn't inherit this display name in most cases,
// because the component may be used elsewhere.
// But it's nice for anonymous functions to inherit the name,
// so that our component-stack generation logic will display their frames.
// An anonymous function generally suggests a pattern like:
// React.memo((props) => {...});
// This kind of inner function is not used elsewhere so the side effect is okay.
if (!type.name && !type.displayName) {
Object.defineProperty(type, 'name', {
value: name,
});
type.displayName = name;
}
},
});
}
return elementType;
}
这段代码其实没干什么,主要就标记了$$typeof: REACT_MEMO_TYPE 。还需要看下 memo 具体对组件的影响。
// packages/react-reconciler/src/ReactFiber.js
export function createFiberFromTypeAndProps(
type: any, // React$ElementType
key: ReactKey,
pendingProps: any,
owner: null | ReactComponentInfo | Fiber,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let fiberTag: WorkTag = FunctionComponent;
// The resolved type is set if we know what the final type will be. I.e. it's not lazy.
let resolvedType = type;
if (typeof type === 'function') {
if (shouldConstruct(type)) {
fiberTag = ClassComponent;
if (__DEV__) {
resolvedType = resolveClassForHotReloading(resolvedType);
}
} else {
if (__DEV__) {
resolvedType = resolveFunctionForHotReloading(resolvedType);
}
}
} else if (typeof type === 'string') {
if (supportsResources && supportsSingletons) {
const hostContext = getHostContext();
fiberTag = isHostHoistableType(type, pendingProps, hostContext)
? HostHoistable
: isHostSingletonType(type)
? HostSingleton
: HostComponent;
} else if (supportsResources) {
const hostContext = getHostContext();
fiberTag = isHostHoistableType(type, pendingProps, hostContext)
? HostHoistable
: HostComponent;
} else if (supportsSingletons) {
fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
} else {
fiberTag = HostComponent;
}
} else {
getTag: switch (type) {
case REACT_ACTIVITY_TYPE:
return createFiberFromActivity(pendingProps, mode, lanes, key);
case REACT_FRAGMENT_TYPE:
return createFiberFromFragment(pendingProps.children, mode, lanes, key);
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode;
mode |= StrictLegacyMode;
if (disableLegacyMode || (mode & ConcurrentMode) !== NoMode) {
// Strict effects should never run on legacy roots
mode |= StrictEffectsMode;
}
break;
case REACT_PROFILER_TYPE:
return createFiberFromProfiler(pendingProps, mode, lanes, key);
case REACT_SUSPENSE_TYPE:
return createFiberFromSuspense(pendingProps, mode, lanes, key);
case REACT_SUSPENSE_LIST_TYPE:
return createFiberFromSuspenseList(pendingProps, mode, lanes, key);
case REACT_LEGACY_HIDDEN_TYPE:
if (enableLegacyHidden) {
return createFiberFromLegacyHidden(pendingProps, mode, lanes, key);
}
// Fall through
case REACT_VIEW_TRANSITION_TYPE:
if (enableViewTransition) {
return createFiberFromViewTransition(pendingProps, mode, lanes, key);
}
// Fall through
case REACT_SCOPE_TYPE:
if (enableScopeAPI) {
return createFiberFromScope(type, pendingProps, mode, lanes, key);
}
// Fall through
case REACT_TRACING_MARKER_TYPE:
if (enableTransitionTracing) {
return createFiberFromTracingMarker(pendingProps, mode, lanes, key);
}
// Fall through
default: {
if (typeof type === 'object' && type !== null) {
switch (type.$$typeof) {
case REACT_CONTEXT_TYPE:
fiberTag = ContextProvider;
break getTag;
case REACT_CONSUMER_TYPE:
fiberTag = ContextConsumer;
break getTag;
// Fall through
case REACT_FORWARD_REF_TYPE:
fiberTag = ForwardRef;
if (__DEV__) {
resolvedType = resolveForwardRefForHotReloading(resolvedType);
}
break getTag;
case REACT_MEMO_TYPE:
fiberTag = MemoComponent;
break getTag;
case REACT_LAZY_TYPE:
fiberTag = LazyComponent;
resolvedType = null;
break getTag;
}
}
let info = '';
let typeString;
if (__DEV__) {
if (
type === undefined ||
(typeof type === 'object' &&
type !== null &&
Object.keys(type).length === 0)
) {
info +=
' You likely forgot to export your component from the file ' +
"it's defined in, or you might have mixed up default and named imports.";
}
if (type === null) {
typeString = 'null';
} else if (isArray(type)) {
typeString = 'array';
} else if (
type !== undefined &&
type.$$typeof === REACT_ELEMENT_TYPE
) {
typeString = `<${
getComponentNameFromType(type.type) || 'Unknown'
} />`;
info =
' Did you accidentally export a JSX literal instead of a component?';
} else {
typeString = typeof type;
}
const ownerName = owner ? getComponentNameFromOwner(owner) : null;
if (ownerName) {
info += '\n\nCheck the render method of `' + ownerName + '`.';
}
} else {
typeString = type === null ? 'null' : typeof type;
}
// The type is invalid but it's conceptually a child that errored and not the
// current component itself so we create a virtual child that throws in its
// begin phase. This is the same thing we do in ReactChildFiber if we throw
// but we do it here so that we can assign the debug owner and stack from the
// element itself. That way the error stack will point to the JSX callsite.
fiberTag = Throw;
pendingProps = new Error(
'Element type is invalid: expected a string (for built-in ' +
'components) or a class/function (for composite components) ' +
`but got: ${typeString}.${info}`,
);
resolvedType = null;
}
}
}
const fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.lanes = lanes;
if (__DEV__) {
fiber._debugOwner = owner;
}
return fiber;
}
可以看到当 $$typeof 为 REACT_MEMO_TYPE 时,使用 MemoComponent 作为 fiberTag 创建 fiber。
// packages/react-reconciler/src/ReactFiberBeginWork.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (__DEV__) {
if (workInProgress._debugNeedsRemount && current !== null) {
// This will restart the begin phase with a new fiber.
const copiedFiber = createFiberFromTypeAndProps(
workInProgress.type,
workInProgress.key,
workInProgress.pendingProps,
workInProgress._debugOwner || null,
workInProgress.mode,
workInProgress.lanes,
);
copiedFiber._debugStack = workInProgress._debugStack;
copiedFiber._debugTask = workInProgress._debugTask;
return remountFiber(current, workInProgress, copiedFiber);
}
}
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true;
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See <https://github.com/facebook/react/pull/19216>.
didReceiveUpdate = true;
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index;
const numberOfForks = getForksAtLevel(workInProgress);
pushTreeId(workInProgress, numberOfForks, slotIndex);
}
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
return updateFunctionComponent(
current,
workInProgress,
Component,
workInProgress.pendingProps,
renderLanes,
);
}
case ClassComponent: {
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return updateClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes);
case HostHoistable:
if (supportsResources) {
return updateHostHoistable(current, workInProgress, renderLanes);
}
// Fall through
case HostSingleton:
if (supportsSingletons) {
return updateHostSingleton(current, workInProgress, renderLanes);
}
// Fall through
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
return updateHostText(current, workInProgress);
case SuspenseComponent:
return updateSuspenseComponent(current, workInProgress, renderLanes);
case HostPortal:
return updatePortalComponent(current, workInProgress, renderLanes);
case ForwardRef: {
return updateForwardRef(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case Fragment:
return updateFragment(current, workInProgress, renderLanes);
case Mode:
return updateMode(current, workInProgress, renderLanes);
case Profiler:
return updateProfiler(current, workInProgress, renderLanes);
case ContextProvider:
return updateContextProvider(current, workInProgress, renderLanes);
case ContextConsumer:
return updateContextConsumer(current, workInProgress, renderLanes);
case MemoComponent: {
return updateMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case SimpleMemoComponent: {
return updateSimpleMemoComponent(
current,
workInProgress,
workInProgress.type,
workInProgress.pendingProps,
renderLanes,
);
}
case IncompleteClassComponent: {
if (disableLegacyMode) {
break;
}
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return mountIncompleteClassComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case IncompleteFunctionComponent: {
if (disableLegacyMode) {
break;
}
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps = resolveClassComponentProps(
Component,
unresolvedProps,
);
return mountIncompleteFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case SuspenseListComponent: {
return updateSuspenseListComponent(current, workInProgress, renderLanes);
}
case ScopeComponent: {
if (enableScopeAPI) {
return updateScopeComponent(current, workInProgress, renderLanes);
}
break;
}
case ActivityComponent: {
return updateActivityComponent(current, workInProgress, renderLanes);
}
case OffscreenComponent: {
return updateOffscreenComponent(
current,
workInProgress,
renderLanes,
workInProgress.pendingProps,
);
}
case LegacyHiddenComponent: {
if (enableLegacyHidden) {
return updateLegacyHiddenComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
case CacheComponent: {
return updateCacheComponent(current, workInProgress, renderLanes);
}
case TracingMarkerComponent: {
if (enableTransitionTracing) {
return updateTracingMarkerComponent(
current,
workInProgress,
renderLanes,
);
}
break;
}
case ViewTransitionComponent: {
if (enableViewTransition) {
return updateViewTransition(current, workInProgress, renderLanes);
}
break;
}
case Throw: {
// This represents a Component that threw in the reconciliation phase.
// So we'll rethrow here. This might be a Thenable.
throw workInProgress.pendingProps;
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
这里看到有 MemoComponent 和 SimpleMemoComponent 两种类型。但是在前面只有 MemoComponent 一种 fiberTag,这两种类型有什么区别?
updateMemoComponent
// packages/react-reconciler/src/ReactFiberBeginWork.js
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
if (current === null) {
const type = Component.type;
if (isSimpleFunctionComponent(type) && Component.compare === null) {
let resolvedType = type;
if (__DEV__) {
resolvedType = resolveFunctionForHotReloading(type);
}
// If this is a plain function component without default props,
// and with only the default shallow comparison, we upgrade it
// to a SimpleMemoComponent to allow fast path updates.
workInProgress.tag = SimpleMemoComponent;
workInProgress.type = resolvedType;
if (__DEV__) {
validateFunctionComponentInDev(workInProgress, type);
}
return updateSimpleMemoComponent(
current,
workInProgress,
resolvedType,
nextProps,
renderLanes,
);
}
const child = createFiberFromTypeAndProps(
Component.type,
null,
nextProps,
workInProgress,
workInProgress.mode,
renderLanes,
);
child.ref = workInProgress.ref;
child.return = workInProgress;
workInProgress.child = child;
return child;
}
const currentChild = ((current.child: any): Fiber); // This is always exactly one child
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (!hasScheduledUpdateOrContext) {
// This will be the props with resolved defaultProps,
// unlike current.memoizedProps which will be the unresolved ones.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// React DevTools reads this flag.
workInProgress.flags |= PerformedWork;
const newChild = createWorkInProgress(currentChild, nextProps);
newChild.ref = workInProgress.ref;
newChild.return = workInProgress;
workInProgress.child = newChild;
return newChild;
}
updateMemoComponent 在组件初始化化时,如果组件为 SimpleFunctionComponent 且无 compare 时,将 tag 转换为 SimpleMemoComponent。
// packages/react-reconciler/src/ReactFiber.js
function shouldConstruct(Component: Function) {
const prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
export function isSimpleFunctionComponent(type: any): boolean {
return (
typeof type === 'function' &&
!shouldConstruct(type) &&
type.defaultProps === undefined
);
}
updateMemoComponent 在组件更新时,会检查是否可复用。
// packages/react-reconciler/src/ReactFiberBeginWork.js
function checkScheduledUpdateOrContext(
current: Fiber,
renderLanes: Lanes,
): boolean {
// Before performing an early bailout, we must check if there are pending
// updates or context.
const updateLanes = current.lanes;
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
// No pending update, but because context is propagated lazily, we need
// to check for a context change before we bail out.
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
return 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.
const prevProps = currentChild.memoizedProps;
// Default to shallow comparison
let compare = Component.compare;
compare = compare !== null ? compare : shallowEqual;
if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
checkScheduledUpdateOrContext 主要检查组件自身 state 和依赖 context 是否更新(没深入去看)。如果更新了,不可复用,否则调用 compare 进行比较;如果 props 不变,返回 bailoutOnAlreadyFinishedWork,否则创建新组件,然后继续检查子节点树。
// packages/react-reconciler/src/ReactFiberBeginWork.js
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
// dependencies存储了该组件依赖的上下文(Context)等信息,直接复用可以避免重新收集
}
if (enableProfilerTimer) {
// Don't update "base" render times for bailouts.
stopProfilerTimerIfRunning(workInProgress);
}
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
// TODO: Once we add back resuming, we should check if the children are
// a work-in-progress set. If so, we need to transfer their effects.
if (current !== null) {
// Before bailing out, check if there are any context changes in
// the children.
lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
return null;
}
} else {
return null;
}
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
bailoutOnAlreadyFinishedWork 的主要作用:
- 复用 dependencies(存储了该组件依赖的上下文(Context)等信息)
- 检查子组件自身 state 和依赖 context 是否更新,如果没有更新则跳过子节点树,否则克隆一份子节点树,继续检查子节点树是否可复用
updateSimpleMemoComponent
// packages/react-reconciler/src/ReactFiberBeginWork.js
function updateSimpleMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens when the inner render suspends.
// We'll need to figure out if this is fine or can cause issues.
if (current !== null) {
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
current.ref === workInProgress.ref &&
// Prevent bailout if the implementation changed due to hot reload.
(__DEV__ ? workInProgress.type === current.type : true)
) {
didReceiveUpdate = false;
// The props are shallowly equal. Reuse the previous props object, like we
// would during a normal fiber bailout.
//
// We don't have strong guarantees that the props object is referentially
// equal during updates where we can't bail out anyway — like if the props
// are shallowly equal, but there's a local state or context update in the
// same batch.
//
// However, as a principle, we should aim to make the behavior consistent
// across different ways of memoizing a component. For example, React.memo
// has a different internal Fiber layout if you pass a normal function
// component (SimpleMemoComponent) versus if you pass a different type
// like forwardRef (MemoComponent). But this is an implementation detail.
// Wrapping a component in forwardRef (or React.lazy, etc) shouldn't
// affect whether the props object is reused during a bailout.
workInProgress.pendingProps = nextProps = prevProps;
if (!checkScheduledUpdateOrContext(current, renderLanes)) {
// The pending lanes were cleared at the beginning of beginWork. We're
// about to bail out, but there might be other lanes that weren't
// included in the current render. Usually, the priority level of the
// remaining updates is accumulated during the evaluation of the
// component (i.e. when processing the update queue). But since since
// we're bailing out early *without* evaluating the component, we need
// to account for it here, too. Reset to the value of the current fiber.
// NOTE: This only applies to SimpleMemoComponent, not MemoComponent,
// because a MemoComponent fiber does not have hooks or an update queue;
// rather, it wraps around an inner component, which may or may not
// contains hooks.
// TODO: Move the reset at in beginWork out of the common path so that
// this is no longer necessary.
workInProgress.lanes = current.lanes;
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See <https://github.com/facebook/react/pull/19216>.
didReceiveUpdate = true;
}
}
}
return updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
updateSimpleMemoComponent 的主要区别在于:
- 先 compare 比较依赖,再检查组件自身 state 和依赖 context 是否更新
- 赋值 props
- 赋值 lanes
为什么 updateSimpleMemoComponent 需要这两个赋值,而 updateMemoComponent 不需要?暂时没太搞懂,先跳过。
定量分析 memo 优化效果
既然 memo 可以跳过组件创建,为什么 react 不给所有组件加上 memo?(官方文档也提到使用React Compiler 的话,可以不用 memo)。如果定量的话,怎样的组件才需要使用 memo?
When you enable React Compiler, you typically don’t need
React.memoanymore.
首先是初始化。使用 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;
}
再以 SimpleMemoComponent 为例。
function updateSimpleMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
renderLanes: Lanes,
): null | Fiber {
if (current !== null) {
const prevProps = current.memoizedProps;
if (
shallowEqual(prevProps, nextProps) &&
current.ref === workInProgress.ref &&
// Prevent bailout if the implementation changed due to hot reload.
(__DEV__ ? workInProgress.type === current.type : true)
) {
didReceiveUpdate = false;
workInProgress.pendingProps = nextProps = prevProps;
if (!checkScheduledUpdateOrContext(current, renderLanes)) {
workInProgress.lanes = current.lanes;
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
didReceiveUpdate = true;
}
}
}
return updateFunctionComponent(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
}
- 命中缓存时:节省时间是 updateFunctionComponent - (shallowEqual + checkScheduledUpdateOrContext + bailoutOnAlreadyFinishedWork + k)。 其中,shallowEqual 时间复杂度是 O(props 数量),checkScheduledUpdateOrContext 时间复杂度是 O(依赖 context 数量)。
- 未命中缓存时:节省时间是 -(shallowEqual + checkScheduledUpdateOrContext)
updateFunctionComponent 和 bailoutOnAlreadyFinishedWork 的差异,实在没有那么多精力分析源码。问了下 AI,updateFunctionComponent: O(组件复杂度 × 子组件数量 × diff算法复杂度),bailoutOnAlreadyFinishedWork: O(子组件数量),代码行数大概多了几百到一千多行。
假设命中缓存概率为 x,节省时间的数学期望为:x(updateFunctionComponent - bailoutOnAlreadyFinishedWork - k) - (shallowEqual + checkScheduledUpdateOrContext),k 的量级可以忽略,约等于 x(updateFunctionComponent - bailoutOnAlreadyFinishedWork) - (shallowEqual + checkScheduledUpdateOrContext)。
这样看来,只要不是命中缓存概率过低(10% 以内)或者 compare 复杂度过高的话,没理由不上 memo 啊。哪位大佬能解释下?
小结
-
只要不是命中缓存概率过低(10% 以内),无脑 memo(暂时还没用过 compare 复杂度较高或依赖 context 数量较高的场景,通常加起来顶天了就 30 个)
-
当满足以下两个条件时,无需重新创建组件:
- compare 比较 props 不变(没传 compare 函数默认为 shallowEqual)
- 组件自身 state 和依赖 context 未更新
useCallback、useMemo
// packages/react/src/ReactHooks.js
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
// packages/react-reconciler/src/ReactFiberHooks.js
const HooksDispatcherOnMount: Dispatcher = {
...
useCallback: mountCallback,
useMemo: mountMemo,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
...
useCallback: updateCallback,
useMemo: updateMemo,
...
};
const HooksDispatcherOnRerender: Dispatcher = {
...
useCallback: updateCallback,
useMemo: updateMemo,
...
};
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
nextCreate();
} finally {
setIsStrictModeForDevtools(false);
}
}
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
const nextValue = nextCreate();
if (shouldDoubleInvokeUserFnsInHooksDEV) {
setIsStrictModeForDevtools(true);
try {
nextCreate();
} finally {
setIsStrictModeForDevtools(false);
}
}
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
可以看到 useCallback 和 useMemo 的区别仅在于:
- useCallback 将第一个参数直接返回,而 useMemo 返回第一个参数的执行结果
- useMemo 在严格模式下会执行两次 nextCreate
useCallback
在官方文档中,说 useCallback 不会阻止创建函数。
Note that
useCallbackdoes not prevent creating the function. You’re always creating a function (and that’s fine!), but React ignores it and gives you back a cached function if nothing changed.
要彻底搞清楚这句话,需要理解 react 的渲染流程。如果暂时以性能优化为目标,可以先跳过这部分。我们只需要知道 react 组件渲染时,组件函数会重新调用。
function ProductPage({ productId, referrer, theme }) {
// 在多次渲染中缓存函数
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // 只要这些依赖没有改变
return (
<div className={theme}>
{/* ShippingForm 就会收到同样的 props 并且跳过重新渲染 */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
假设有上面这样一个组件。每次渲染时,都会调用 ProductPage(…),callback 自然也重新创建了一次。不过当 deps 没变时,会返回 hook.memoizedState 中存储的上一次创建的 callback。所以如果 callback 不是其他 hooks 的依赖或作为 props 传给 memo 组件时,使用 useCallback 是负优化的。
useMemo
如何衡量计算过程的开销是否昂贵?
In general, unless you’re creating or looping over thousands of objects, it’s probably not expensive.
文档用到的说法叫循环 thousands of objects,这类说法就很恶心,特别遇到 ld 要定量分析的时候。
只看 updateMemo 的话,命中缓存和不使用 useMemo 的代码区别主要是:
- 命中缓存时:运行代码多出 updateWorkInProgressHook + areHookInputsEqual - nextCreate
- 未命中缓存时:运行代码多出 updateWorkInProgressHook + areHookInputsEqual
function updateWorkInProgressHook(): Hook {
// This function is used both for updates and for re-renders triggered by a
// render phase update. It assumes there is either a current hook we can
// clone, or a work-in-progress hook from a previous render pass that we can
// use as a base.
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
if (nextCurrentHook === null) {
const currentFiber = currentlyRenderingFiber.alternate;
if (currentFiber === null) {
// This is the initial render. This branch is reached when the component
// suspends, resumes, then renders an additional hook.
// Should never be reached because we should switch to the mount dispatcher first.
throw new Error(
'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
);
} else {
// This is an update. We should always have a current hook.
throw new Error('Rendered more hooks than during the previous render.');
}
}
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
areHookInputsEqual 的时间复杂度是 O(n) ,updateWorkInProgressHook 的运行行数为 50 以内。假设命中缓存概率为 x,nextCreate 运行行数为 y,则使用 useMemo 的运行行数的数学期望是 x(50+n-y) + (1-x)(50+n) = 50+n−xy,即:命中缓存概率 * nextCreate 运行行数 > 50 + 依赖数,即可考虑使用 useMemo,就算考虑 mountMemo 初始化的损耗,命中缓存概率 * nextCreate 运行行数在50~100行往上就可以了,根本不需要像文档说的一样要到 thousands of。(可能有别的地方没有考虑到,欢迎斧正)
小结
- 如果 callback 不是其他 hooks 的依赖或作为 props 传给 memo 组件时,使用 useCallback 是负优化的
- 命中缓存概率 * nextCreate 运行行数在50~100行往上就可以考虑使用 useMemo