深入React 18源码useMemo useCallback memo用法及区别分析

开篇

哈喽大咖好,我是跑手,最近在做 React 相关的组件搭建,因为涉及到大量的图形计算以及页面渲染,所以特意翻了下性能优化相关的hooks使用,如 useMemouseCallback 和 memo。在这篇文章中,我们将探讨这些功能的用法和区别,并通过源码分析来理解它们的工作原理,开整!

用法

useMemo

useMemo 是一个用于优化性能的 React 钩子。它可以帮助我们避免在组件重新渲染时执行昂贵的计算。useMemo 接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useMemo 会重新计算并返回新的值。否则,它将返回上一次计算的值。

一个简单的例子:

  1. import React, { useMemo } from “react”;
  2. function ExpensiveComponent({ a, b }) {
  3.      const result = useMemo(() => {
  4.      console.log(“Expensive calculation…”);
  5.      return a * b;
  6.      }, [a, b]);
  7.      return <div>Result: {result}</div>;
  8. }

我们创建了一个名为 ExpensiveComponent 的组件,它接受两个属性 a 和 b 并使用 useMemo 钩子来计算 a 和 b 的乘积。当 a 或 b 发生变化时,useMemo 会重新计算结果。否则,它将返回上一次计算的值,避免了不必要的计算。

useCallback

useCallback 是另一个用于优化性能的 React 钩子。它可以帮助我们避免在组件重新渲染时创建新的函数实例。useCallback 接受两个参数:一个函数和一个依赖数组。当依赖数组中的值发生变化时,useCallback 会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。

再看一个简单的例子:

  1. import React, { useCallback } from “react”;
  2. function ButtonComponent({ onClick, children }) {
  3.      return <button onClick={onClick}>{children}</button>;
  4. }
  5. function ParentComponent() {
  6.      const handleClick = useCallback(() => {
  7.      console.log(“Button clicked”);
  8.      }, []);
  9.      return (
  10.      <div>
  11.          <ButtonComponent onClick={handleClick}>Click me</ButtonComponent>
  12.      </div>
  13.      );
  14. }

在这个例子中,我们创建了一个名为 ButtonComponent 的组件,它接受一个 onClick 函数属性。我们还创建了一个名为 ParentComponent 的组件,它使用 useCallback 钩子来创建一个 handleClick 函数。当 ParentComponent 重新渲染时,useCallback 会返回上一次创建的 handleClick 函数实例,避免了不必要的函数创建。

memo

memo 是一个用于优化性能的 React 高阶组件。它可以帮助我们避免在父组件重新渲染时重新渲染子组件。memo 接受一个组件作为参数,并返回一个新的组件。当新组件的属性发生变化时,它会重新渲染。否则,它将跳过渲染并返回上一次渲染的结果。

继续举例子:

  1. import React, { memo } from “react”;
  2. const ChildComponent = memo(function ChildComponent({ text }) {
  3.      console.log(“ChildComponent rendered”);
  4.      return <div>{text}</div>;
  5. });
  6. function ParentComponent({ showChild }) {
  7.      return (
  8.      <div>
  9.          {showChild && <ChildComponent text=“Hello, world!” />}
  10.          <button onClick={() => setShowChild(!showChild)}>Toggle child</button>
  11.      </div>
  12.      );
  13. }

在这个例子中,我们创建了一个名为 ChildComponent 的组件,并使用 memo 高阶组件对其进行了优化。我们还创建了一个名为 ParentComponent 的组件,它可以切换 ChildComponent 的显示。当 ParentComponent 重新渲染时,ChildComponent 的属性没有发生变化,因此它不会重新渲染。

区别

用法都很清楚了,接下来总结一下它们之间的区别:

  • useMemo 用于避免在组件重新渲染时执行昂贵的计算,只有在依赖发生变化时重新计算值。
  • useCallback 用于避免在组件重新渲染时创建新的函数实例,只有在依赖发生变化时返回新的函数实例。
  • memo 用于避免在父组件重新渲染时重新渲染子组件,只有在属性发生变化时重新渲染组件。

虽然这些功能都可以帮助我们优化性能,但它们的使用场景和工作原理有所不同。在实际开发中,需要因地制宜合理选用。

源码分析

为了更深入地了解 useMemouseCallback 和 memo 的工作原理,我们将继续分析 React 18 的源码。我们将关注这些功能的核心逻辑,并详细解释它们的功能。

调度器

众所周知,在React hooks的体系中,每个钩子都有自己各个阶段的执行逻辑,并且存到对应的Dispatcher中。

就拿useMemo来举例:

  1. // 挂载时的调度器
  2. const HooksDispatcherOnMount: Dispatcher = {
  3.      // useMemo 挂载时的执行函数
  4.      useMemo: mountMemo,
  5.      // other hooks…
  6. };
  7. // 数据更新时的调度器
  8. const HooksDispatcherOnUpdate: Dispatcher = {
  9.      // useMemo 挂载时的执行函数
  10.      useMemo: updateMemo,
  11.      // other hooks…
  12. };
  13. // 其他生命周期调度器…

上面代码可以看出,useMemo 在挂载时执行了的是 mountMemo, 而在更新数据时执行的是 updateMemo。但为了更好了解 useMemouseCallback 和 memo 的区别,我们只看更新部分就足够了。

useMemo 源码分析

源码在packages/react-reconciler/src/ReactFiberHooks.js 中可以找到:

  1. function updateMemo<T>(
  2.      nextCreate: () => T,
  3.      deps: Array<mixed> | void | null,
  4. ): T {
  5.      const hook = updateWorkInProgressHook();
  6.      const nextDeps = deps === undefined ? null : deps;
  7.      const prevState = hook.memoizedState;
  8.      // Assume these are defined. If they’re not, areHookInputsEqual will warn.
  9.      if (nextDeps !== null) {
  10.      const prevDeps: Array<mixed> | null = prevState[1];
  11.      if (areHookInputsEqual(nextDeps, prevDeps)) {
  12.          return prevState[0];
  13.      }
  14.      }
  15.      if (shouldDoubleInvokeUserFnsInHooksDEV) {
  16.      nextCreate();
  17.      }
  18.      const nextValue = nextCreate();
  19.      hook.memoizedState = [nextValue, nextDeps];
  20.      return nextValue;
  21. }

在 updateMemo 的实现中,有一个关键函数 areHookInputsEqual,它用于比较依赖项数组:

  1. function areHookInputsEqual(
  2.      nextDeps: Array<mixed>,
  3.      prevDeps: Array<mixed> | null,
  4. ): boolean {
  5.      if (__DEV__) {
  6.      if (ignorePreviousDependencies) {
  7.          // Only true when this component is being hot reloaded.
  8.          return false;
  9.      }
  10.      }
  11.      if (prevDeps === null) {
  12.      if (__DEV__) {
  13.          console.error(
  14.          ‘%s received a final argument during this render, but not during ‘ +
  15.              ‘the previous render. Even though the final argument is optional, ‘ +
  16.              ‘its type cannot change between renders.’,
  17.          currentHookNameInDev,
  18.          );
  19.      }
  20.      return false;
  21.      }
  22.      if (__DEV__) {
  23.      // Don’t bother comparing lengths in prod because these arrays should be
  24.      // passed inline.
  25.      if (nextDeps.length !== prevDeps.length) {
  26.          console.error(
  27.          ‘The final argument passed to %s changed size between renders. The ‘ +
  28.              ‘order and size of this array must remain constant.\n\n’ +
  29.              ‘Previous: %s\n’ +
  30.              ‘Incoming: %s’,
  31.          currentHookNameInDev,
  32.          `[${prevDeps.join(‘, ‘)}]`,
  33.      `[${nextDeps.join(‘, ‘)}]`,
  34.          );
  35.      }
  36.      }
  37.      // $FlowFixMe[incompatible-use] found when upgrading Flow
  38.      for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
  39.      // $FlowFixMe[incompatible-use] found when upgrading Flow
  40.      if (is(nextDeps[i], prevDeps[i])) {
  41.          continue;
  42.      }
  43.      return false;
  44.      }
  45.      return true;
  46. }

areHookInputsEqual 函数接受两个依赖项数组 nextDeps 和 prevDeps。它首先检查两个数组的长度是否相等,如果不相等,将在开发模式下发出警告。然后,它遍历数组并使用 is 函数(类似于 Object.is)逐个比较元素。如果发现任何不相等的元素,函数将返回 false。否则,返回 true

这个函数在 useMemo 的实现中起到了关键作用,因为它决定了是否需要重新计算值。如果依赖项数组相等,useMemo 将返回上一次计算的值;否则,它将执行 nextCreate 函数并返回一个新的值。

useCallback 源码分析

由于 useCallback 和 useMemo 实现一致,其原理都是通过areHookInputsEqual 函数进行依赖项比对,区别在于 useMemo 返回是新数据对象,而 useCallback 返回是回调函数。源码如下:

  1. function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  2.      const hook = updateWorkInProgressHook();
  3.      const nextDeps = deps === undefined ? null : deps;
  4.      const prevState = hook.memoizedState;
  5.      if (nextDeps !== null) {
  6.      const prevDeps: Array<mixed> | null = prevState[1];
  7.      if (areHookInputsEqual(nextDeps, prevDeps)) {
  8.          return prevState[0];
  9.      }
  10.      }
  11.      hook.memoizedState = [callback, nextDeps];
  12.      return callback;
  13. }

memo 源码分析

在 memo 的实现中,有一个关键函数 updateMemoComponent,它用于更新 memo 组件。这个函数位于 packages/react-reconciler/src/ReactFiberBeginWork.js 文件中:

  1. function updateMemoComponent(
  2.      current: Fiber | null,
  3.      workInProgress: Fiber,
  4.      Component: any,
  5.      nextProps: any,
  6.      updateLanes: Lanes,
  7.      renderLanes: Lanes,
  8. ): null | Fiber {
  9.      if (current !== null) {
  10.      // …
  11.      const prevProps = current.memoizedProps;
  12.      const compare = Component.compare;
  13.      const compareFn = compare !== null ? compare : shallowEqual;
  14.      if (compareFn(prevProps, nextProps)) {
  15.          return bailoutOnAlreadyFinishedwork(
  16.          current,
  17.          workInProgress,
  18.          renderLanes,
  19.          );
  20.      }
  21.      }
  22.      // …render the component and return the result
  23. }

updateMemoComponent 函数首先检查当前组件是否具有上一次的属性 prevProps。如果存在,它将获取 memo 组件的比较函数 compare。如果没有提供比较函数,React 将使用默认的浅比较函数 shallowEqual

接下来,React 使用比较函数来检查上一次的属性 prevProps 是否与新的属性 nextProps 相等。如果相等,React 将调用 bailoutOnAlreadyFinishedWork 函数来阻止组件重新渲染。否则,它将继续渲染组件并返回结果。

bailoutOnAlreadyFinishedWork 函数的实现位于同一个文件中,它的核心逻辑如下:

  1.      function bailoutOnAlreadyFinishedWork(
  2.          current: Fiber | null,
  3.          workInProgress: Fiber,
  4.          renderLanes: Lanes,
  5.      ): null | Fiber {
  6.          if (current !== null) {
  7.          // Reuse previous dependencies
  8.          workInProgress.dependencies = current.dependencies;
  9.          }
  10.          // …some code
  11.          // Check if the children have any pending work
  12.          if ((workInProgress.childLanes & renderLanes) !== NoLanes) {
  13.          // …some code
  14.          } else {
  15.          // The children don’t have any work. Set the bailout state.
  16.          workInProgress.lanes = NoLanes;
  17.          workInProgress.childLanes = NoLanes;
  18.          return null;
  19.          }
  20.          // …some code
  21.      }

bailoutOnAlreadyFinishedWork 函数首先复用上一次的依赖项。然后,它检查子组件是否有任何待处理的工作。如果没有,它将设置 workInProgress.lanes 和 workInProgress.childLanes 为 NoLanes,并返回 null,从而阻止组件重新渲染。

总结

在这篇文章中,我们深入分析了 React 18 中的 useMemouseCallback 和 memo 功能的源码。希望这篇文章能帮助你更好在实际项目中应用它们。

以上就是深入React 18源码useMemo useCallback memo用法及区别分析的详细内容,更多关于React useMemo useCallback memo的资料请关注我们其它相关文章!

标签

发表评论