useCallbackの理解とアプリケーション

概念

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
复制代码

意味

概念

1.メモ化された コールバック関数を返し ます。
2.インラインコールバック関数と依存関係の配列をパラメーターとして渡すと useCallback、メモ化されたバージョンのコールバック関数が返されます。これは、依存関係が変更された場合にのみ更新されます。最適化された子コンポーネントにコールバックを渡し、参照の同等性を使用して不要なレンダリングを回避する場合に便利です shouldComponentUpdate(たとえば)。
useCallback(fn, deps) と同等 useMemo(() => fn, deps)です。英語の文書

ソースコードの実装

フックの2つの重要な呼び出しフェーズ

ソースファイル:ReactFiberHooks.js

特記事項:レンダリングステージは展開されません

const HooksDispatcherOnRerender: Dispatcher = {
  useCallback: updateCallback,
  // ...
};
复制代码
  1. HooksDispatcherOnMountステージは、最初のレンダリングが初期化されるときに呼び出されるフックメソッドに対応し、mountCallbackそれぞれ待機中のフックに対応します。
  2. HooksDispatcherOnUpdateステージは、setXXX関数が更新と再レンダリングをトリガーする更新ステージに対応し、それぞれupdateCallback待機中のフックに対応します。
// 挂载阶段
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback, // useCallback 挂载(mount)
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,

  unstable_isNewReconciler: enableNewReconciler,
};

// 更新阶段
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback, // useCallback 更新(update)
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
};
复制代码

useCallback2段階

// 挂载阶段
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 插入memoizedState属性
  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 (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 相等直接返回旧的函数指针
        return prevState[0];
      }
    }
  }
  // 有依赖更新则生成新的memoizedState;直接返回新的函数
  hook.memoizedState = [callback, nextDeps];
  return callback;
}
复制代码

誤解しやすい

1.コンポーネントの更新時にコールバック関数が再作成されるかどうか

結論:再作成する必要があります

const DemoComponent = ()=>{ 
    // 先创建了一个函数 funB
    const callbackFunc = () => {};
    // 函数 funB 当作第一个参数传入 useCallback 中;deps为[]
    const funA = useCallback(callbackFunc, []);
    // 省略代码...
}
复制代码

結論:callbackFunc関数が毎回新しく作成されることを確認するのは難しくありません;即useCallback不是为了减少函数创建次数再結合段階ではuseCallbackupdatefunAが更新されたかどうか(修正が指しているかどうか)は、変更があるかどうかにcallbackFunc依存すると結論付けることができますdeps

2.すべての機能が1つのレイヤーにパッケージ化されているかどうかuseCallback

useCallbackは使用されません

import { useState } from "react";

const SonComponent = ({ handleClick }) => {
  return <button onClick={handleClick}>Click!</button>;
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount((count) => count + 1);
  };

  return (
    <div>
      <p>{count}</p>
      <SonComponent handleClick={handleClick} />
    </div>
  );
}

export default ParentComponent;
复制代码

パフォーマンスグラフ:
image.png

useCallbackを使用する

import { useState, useCallback } from "react";

const SonComponent = ({ handleClick }) => {
  return <button onClick={handleClick}>Click!</button>;
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount((count) => count + 1);
  },[]);

  return (
    <div>
      <p>{count}</p>
      <SonComponent handleClick={handleClick} />
    </div>
  );
}

export default ParentComponent;
复制代码

パフォーマンスチャート:
image.png
結論:パフォーマンスの図から、useCallbackを追加しない方がよいです。理由:useCallBackを追加すると、冗長な関数が実行されます。

使用するシーン

1.React.memoで使用する

import { useState, useCallback, memo } from "react";

const SonExpenceComponent = memo(({ handleClick }) => {
  return <button onClick={handleClick}>Click!</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return (
    <div>
      <p>{count}</p>
      <SonExpenceComponent handleClick={handleClick} />
    </div>
  );
}

export default ParentComponent;
复制代码

問題があります:depsのカウント値が変更されるたびに、SonExpenceComponentコンポーネントが更新されます

解決策1:公式ウェブサイトで提供されているスキームは、useEffectとuseRefの組み合わせを使用しています

import { useState, useCallback, useRef, useEffect, memo } from "react";

const SonExpenceComponent = memo(({ handleClick }) => {
  return <button onClick={handleClick}>Click!</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  const handleClick = useCallback(() => {
    setCount(countRef.current);
  }, []);
  
  // 同步countRef
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  return (
    <div>
      <p>{count}</p>
      <SonExpenceComponent handleClick={handleClick} />
    </div>
  );
}

export default ParentComponent;
复制代码

解決策2:ahooksのuseMemoizedFn

import { useMemo, useRef } from 'react';

type noop = (this: any, ...args: any[]) => any;

type PickFunction<T extends noop> = (
  this: ThisParameterType<T>,
  ...args: Parameters<T>
) => ReturnType<T>;

function useMemoizedFn<T extends noop>(fn: T) {
  const fnRef = useRef<T>(fn);
  // 永远拿到最新fn
  fnRef.current = useMemo(() => fn, [fn]);

  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current;
}

export default useMemoizedFn;
复制代码

注:上記はすべて擬似コードであり、デフォルトのサブコンポーネントは比較的オーバーヘッドの大きいコンポーネントです。

実用的なケースを開発する

1.ロングリストレンダリング

最適化前:

import React, { useState} from "react";

const SonExpenceComponent =({ addCount }) => (
  <div>
    <button onClick={addCount}>Add</button>
    {new Array(1000).fill("item").map((i, index) => (
      <div>{i + index}</div>
    ))}
  </div>
);

const App = () => {
  const [count, setCount] = useState(0);

  const addCount = () => {
    setCount((count) => count + 1);
  };

  return (
    <>
      <p>Count: {count}</p>
      <SonExpenceComponent addCount={addCount} />
    </>
  );
};

export default App;
复制代码

image.png最適化:

import React, { useState, useCallback,memo } from "react";

const SonExpenceComponent =memo(({ addCount }) => (
  <div>
    <button onClick={addCount}>Add</button>
    {new Array(1000).fill("item").map((i, index) => (
      <div>{i + index}</div>
    ))}
  </div>
));

const App = () => {
  const [count, setCount] = useState(0);

  const addCount = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <>
      <p>Count: {count}</p>
      <SonExpenceComponent addCount={addCount} />
    </>
  );
};

export default App;
复制代码

image.png結論:useCallbackとメモを追加した後、ボタンをクリックすると、アプリコンポーネントの全体的なレンダリング時間が1ミリ秒であることがわかります。これは、全体的なレンダリング時間を追加しない場合の全体的なレンダリング時間である11.8ミリ秒よりもはるかに短い時間です。

結論は

1. React.memoでは高価なコンポーネントを使用できます。2
。パフォーマンスが大幅に向上しない限り、導入する必要はありません。useCallback

useMemoの原理

おすすめ

転載: juejin.im/post/7079663951214018596