React18中useMemo、useCallback和memo怎么使用

寻技术 React 2023年11月25日 117

本篇内容介绍了“React18中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/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.

' +
          'Previous: %s
' +
          '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/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
,从而阻止组件重新渲染。
关闭

用微信“扫一扫”