深入React 18源码useMemo useCallback memo用法及区别分析
开篇
哈喽大咖好,我是跑手,最近在做 React
相关的组件搭建,因为涉及到大量的图形计算以及页面渲染,所以特意翻了下性能优化相关的hooks使用,如 useMemo
、useCallback
和 memo
。在这篇文章中,我们将探讨这些功能的用法和区别,并通过源码分析来理解它们的工作原理,开整!
用法
useMemo
useMemo
是一个用于优化性能的 React 钩子。它可以帮助我们避免在组件重新渲染时执行昂贵的计算。useMemo
接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useMemo
会重新计算并返回新的值。否则,它将返回上一次计算的值。
一个简单的例子:
import React, { useMemo } from "react";
function ExpensiveComponent({ a, b }) {
const result = useMemo(() => {
console.log("Expensive calculation...");
return a * b;
}, [a, b]);
return <div>Result: {result}</div>;
}
我们创建了一个名为 ExpensiveComponent
的组件,它接受两个属性 a
和 b
并使用 useMemo
钩子来计算 a
和 b
的乘积。当 a
或 b
发生变化时,useMemo
会重新计算结果。否则,它将返回上一次计算的值,避免了不必要的计算。
useCallback
useCallback
是另一个用于优化性能的 React 钩子。它可以帮助我们避免在组件重新渲染时创建新的函数实例。useCallback
接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useCallback
会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。
再看一个简单的例子:
import React, { useCallback } from "react";
function ButtonComponent({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
function ParentComponent() {
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
return (
<div>
<ButtonComponent onClick={handleClick}>Click me</ButtonComponent>
</div>
);
}
在这个例子中,我们创建了一个名为 ButtonComponent
的组件,它接受一个 onClick
函数属性。我们还创建了一个名为 ParentComponent
的组件,它使用 useCallback
钩子来创建一个 handleClick
函数。当 ParentComponent
重新渲染时,useCallback
会返回上一次创建的 handleClick
函数实例,避免了不必要的函数创建。
memo
memo
是一个用于优化性能的 React 高阶组件。它可以帮助我们避免在父组件重新渲染时重新渲染子组件。memo
接受一个组件作为参数,并返回一个新的组件。当新组件的属性发生变化时,它会重新渲染。否则,它将跳过渲染并返回上一次渲染的结果。
继续举例子:
import React, { memo } from "react";
const ChildComponent = memo(function ChildComponent({ text }) {
console.log("ChildComponent rendered");
return <div>{text}</div>;
});
function ParentComponent({ showChild }) {
return (
<div>
{showChild && <ChildComponent text="Hello, world!" />}
<button onClick={() => setShowChild(!showChild)}>Toggle child</button>
</div>
);
}
在这个例子中,我们创建了一个名为 ChildComponent
的组件,并使用 memo
高阶组件对其进行了优化。我们还创建了一个名为 ParentComponent
的组件,它可以切换 ChildComponent
的显示。当 ParentComponent
重新渲染时,ChildComponent
的属性没有发生变化,因此它不会重新渲染。
区别
用法都很清楚了,接下来总结一下它们之间的区别:
useMemo
用于避免在组件重新渲染时执行昂贵的计算,只有在依赖发生变化时重新计算值。useCallback
用于避免在组件重新渲染时创建新的函数实例,只有在依赖发生变化时返回新的函数实例。memo
用于避免在父组件重新渲染时重新渲染子组件,只有在属性发生变化时重新渲染组件。
虽然这些功能都可以帮助我们优化性能,但它们的使用场景和工作原理有所不同。在实际开发中,需要因地制宜合理选用。
源码分析
为了更深入地了解 useMemo
、useCallback
和 memo
的工作原理,我们将继续分析 React 18
的源码。我们将关注这些功能的核心逻辑,并详细解释它们的功能。
调度器
众所周知,在React hooks
的体系中,每个钩子都有自己各个阶段的执行逻辑,并且存到对应的Dispatcher
中。
就拿useMemo来举例:
// 挂载时的调度器
const HooksDispatcherOnMount: Dispatcher = {
// useMemo 挂载时的执行函数
useMemo: mountMemo,
// other hooks...
};
// 数据更新时的调度器
const HooksDispatcherOnUpdate: Dispatcher = {
// useMemo 挂载时的执行函数
useMemo: updateMemo,
// other hooks...
};
// 其他生命周期调度器...
上面代码可以看出,useMemo
在挂载时执行了的是 mountMemo
, 而在更新数据时执行的是 updateMemo
。但为了更好了解 useMemo
、useCallback
和 memo
的区别,我们只看更新部分就足够了。
useMemo 源码分析
源码在packages/react-reconciler/class="lazy" data-src/ReactFiberHooks.js
中可以找到:
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];
}
}
if (shouldDoubleInvokeUserFnsInHooksDEV) {
nextCreate();
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
在 updateMemo
的实现中,有一个关键函数 areHookInputsEqual
,它用于比较依赖项数组:
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
): boolean {
if (__DEV__) {
if (ignorePreviousDependencies) {
// Only true when this component is being hot reloaded.
return false;
}
}
if (prevDeps === null) {
if (__DEV__) {
console.error(
'%s received a final argument during this render, but not during ' +
'the previous render. Even though the final argument is optional, ' +
'its type cannot change between renders.',
currentHookNameInDev,
);
}
return false;
}
if (__DEV__) {
// Don't bother comparing lengths in prod because these arrays should be
// passed inline.
if (nextDeps.length !== prevDeps.length) {
console.error(
'The final argument passed to %s changed size between renders. The ' +
'order and size of this array must remain constant.\n\n' +
'Previous: %s\n' +
'Incoming: %s',
currentHookNameInDev,
`[${prevDeps.join(', ')}]`,
`[${nextDeps.join(', ')}]`,
);
}
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
areHookInputsEqual
函数接受两个依赖项数组 nextDeps
和 prevDeps
。它首先检查两个数组的长度是否相等,如果不相等,将在开发模式下发出警告。然后,它遍历数组并使用 is
函数(类似于 Object.is
)逐个比较元素。如果发现任何不相等的元素,函数将返回 false
。否则,返回 true
。
这个函数在 useMemo
的实现中起到了关键作用,因为它决定了是否需要重新计算值。如果依赖项数组相等,useMemo
将返回上一次计算的值;否则,它将执行 nextCreate
函数并返回一个新的值。
useCallback 源码分析
由于 useCallback
和 useMemo
实现一致,其原理都是通过areHookInputsEqual
函数进行依赖项比对,区别在于 useMemo
返回是新数据对象,而 useCallback
返回是回调函数。源码如下:
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;
}
memo 源码分析
在 memo
的实现中,有一个关键函数 updateMemoComponent
,它用于更新 memo
组件。这个函数位于 packages/react-reconciler/class="lazy" data-src/ReactFiberBeginWork.js
文件中:
function updateMemoComponent(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
nextProps: any,
updateLanes: Lanes,
renderLanes: Lanes,
): null | Fiber {
if (current !== null) {
// ...
const prevProps = current.memoizedProps;
const compare = Component.compare;
const compareFn = compare !== null ? compare : shallowEqual;
if (compareFn(prevProps, nextProps)) {
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
}
}
// ...render the component and return the result
}
updateMemoComponent
函数首先检查当前组件是否具有上一次的属性 prevProps
。如果存在,它将获取 memo
组件的比较函数 compare
。如果没有提供比较函数,React 将使用默认的浅比较函数 shallowEqual
。
接下来,React 使用比较函数来检查上一次的属性 prevProps
是否与新的属性 nextProps
相等。如果相等,React 将调用 bailoutOnAlreadyFinishedWork
函数来阻止组件重新渲染。否则,它将继续渲染组件并返回结果。
bailoutOnAlreadyFinishedWork
函数的实现位于同一个文件中,它的核心逻辑如下:
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): null | Fiber {
if (current !== null) {
// Reuse previous dependencies
workInProgress.dependencies = current.dependencies;
}
// ...some code
// Check if the children have any pending work
if ((workInProgress.childLanes & renderLanes) !== NoLanes) {
// ...some code
} else {
// The children don't have any work. Set the bailout state.
workInProgress.lanes = NoLanes;
workInProgress.childLanes = NoLanes;
return null;
}
// ...some code
}
bailoutOnAlreadyFinishedWork
函数首先复用上一次的依赖项。然后,它检查子组件是否有任何待处理的工作。如果没有,它将设置 workInProgress.lanes
和 workInProgress.childLanes
为 NoLanes
,并返回 null
,从而阻止组件重新渲染。
总结
在这篇文章中,我们深入分析了 React 18 中的 useMemo
、useCallback
和 memo
功能的源码。希望这篇文章能帮助你更好在实际项目中应用它们。
以上就是深入React 18源码useMemo useCallback memo用法及区别分析的详细内容,更多关于React useMemo useCallback memo的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341