useCallback理解与应用

概念

实例

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

定义

概念

1.返回一个 memoized 回调函数。
2.把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
英文文档

源码实现

hooks两个重要调用阶段

源码文件:ReactFiberHooks.js

特别说明:render阶段不作展开

const HooksDispatcherOnRerender: Dispatcher = {
  useCallback: updateCallback,
  // ...
};
复制代码
  1. HooksDispatcherOnMount阶段对应第一次渲染初始化时候调用的hooks方法,分别对应了mountCallback等hooks。
  2. HooksDispatcherOnUpdate阶段对应setXXX函数触发更新重新渲染的更新阶段,分别对应了updateCallback等hooks
// 挂载阶段
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,
};
复制代码

useCallback两个阶段

// 挂载阶段
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. callback函数是否在组件更新是重新创建

结论:一定重新创建

举例

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

结论:不难看出callbackFunc函数每次都会新建,即useCallback不是为了减少函数创建次数;再结合useCallbackupdate阶段可以得出结论funA是否有更新(指正是否指向callbackFunc)要看deps是否有变化

2.是否所有函数包一层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的count值都会变导致SonExpenceComponent组件更新

解法一:官网给出的方案使用 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;
复制代码

解法二: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 和 memo 后,点击 button,可以看到 App 组件整体的渲染时间为 1ms,比未添加整体渲染时间 11.8 ms 减少非常多。

结论

1.开销昂贵组件可以配合React.memo使用
2.除非有明显性能提升,不然没必要引入useCallback

useMemo原理

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享