React 中的 useCallback 是什么以及何时使用它?

iRo*_*tia 31 javascript typescript reactjs react-native

我已经阅读了几篇关于何时使用和何时不使用的文章,useCallbackuseMemo我大多看到的是非常contrived代码。我在查看我公司的代码时发现有人这样做了:

const takePhoto = useCallback(() => {
    launchCamera({ mediaType: "photo", cameraType: "front" }, onPickImage);
  }, []);

  const pickPhotoFromLibrary = async () => {
    launchImageLibrary({ mediaType: "photo" }, onPickImage);
  }

  const onUploadPress = useCallback(() => {
    Alert.alert(
      "Upload Photo",
      "From where would you like to take your photo?",
      [
        { text: "Camera", onPress: () => takePhoto() },
        { text: "Library", onPress: () => pickPhotoFromLibrary() },
      ]
    );
  }, [pickPhotoFromLibrary, takePhoto]);
Run Code Online (Sandbox Code Playgroud)

这是 onUploadPress 的调用方式:

<TouchableOpacity
   style={styles.retakeButton}
   onPress={onUploadPress}
>
Run Code Online (Sandbox Code Playgroud)

您认为这是正确的调用方式吗?根据我对这些文章的理解,这看起来是不正确的。有人可以告诉我何时使用useCallback并且还可以用useCallback更人性化的术语进行解释吗?

我读过的文章:何时使用 useMemo 和 useCallback

You*_*mar 28

useCallback接受一个函数作为第一个参数并返回它的记忆版本(就其内存位置而言,而不是在内部完成的计算)。这意味着每次组件重新渲染时,返回的函数不会在新的内存引用上重新创建,而组件内的普通函数则会这样做。

如果 的依赖数组内的变量之一useCalback(其第二个参数)发生更改,则返回的函数将在新的内存引用上重新创建。

现在,你为什么要为这个烦恼呢?好吧,每当组件内函数的正常行为对您来说有问题时,这是值得的。

例如,如果您在 的依赖数组中具有该函数useEffect,或者将其传递给使用 记忆的组件memo

useEffect第一次渲染时以及每当其依赖项数组中的一个变量发生变化时,都会调用an 的回调。由于通常会在每次渲染时创建该函数的新版本,因此回调可能会被无限调用。所以useCallback用来记忆它。

仅当其或更改时,记忆组件才会memo重新呈现,而不是因为其父级重新呈现。由于通常会创建该传递函数的新版本,因此当父组件重新渲染时,子组件会获得新的引用,因此它会重新渲染。所以用来记忆它。statepropspropsuseCallback

为了说明这一点,我创建了下面的 React 应用程序。单击该按钮可触发父级的重新渲染并观看控制台。希望它能解决问题!

const MemoizedChildWithMemoizedFunctionInProps = React.memo(
  ({ memoizedDummyFunction }) => {
    console.log("MemoizedChildWithMemoizedFunctionInProps renders");
    return <div></div>;
  }
);

const MemoizedChildWithNonMemoizedFunctionInProps = React.memo(
  ({ nonMemoizedDummyFunction }) => {
    console.log("MemoizedChildWithNonMemoizedFunctionInProps renders");
    return <div></div>;
  }
);

const NonMemoizedChild = () => {
  console.log("Non memoized child renders");
  return <div></div>;
};

const Parent = () => {
  const [state, setState] = React.useState(true);

  const nonMemoizedFunction = () => {};
  
  const memoizedFunction = React.useCallback(() => {}, []);
  
  React.useEffect(() => {
    console.log("useEffect callback with nonMemoizedFunction runs");
  }, [nonMemoizedFunction]);

  React.useEffect(() => {
    console.log("useEffect callback with memoizedFunction runs");
  }, [memoizedFunction]);
  
  console.clear();
  console.log("Parent renders");
  
  
  return (
    <div>
      <button onClick={() => setState((prev) => !prev)}>Toggle state</button>
      <MemoizedChildWithMemoizedFunctionInProps
        memoizedFunction={memoizedFunction}
      />
      <MemoizedChildWithNonMemoizedFunctionInProps
        nonMemoizedFunction={nonMemoizedFunction}
      />
      <NonMemoizedChild />
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

要知道记忆并不是免费的,所以做错了会更糟糕。在您的情况下,使用useCallbackforonUploadPress是一种浪费,因为非记忆函数pickPhotoFromLibrary位于依赖项数组中。TouchableOpacity另外,如果不使用 来记忆,那就太浪费了memo,我不确定它是不是。

作为旁注,有useMemo,其行为和用途类似于出于相同原因useCallback记忆非函数但引用的值,例如objectsarrays,或者记忆您不想在渲染之间重复的繁重计算的任何结果。

一个很好的资源,可以深入了解 React 渲染过程,了解何时进行记忆以及如何做好它:React Render


Ahm*_*rek 15

简而言之,useCallback用于将函数引用保存在组件渲染之外的某个位置,以便我们可以再次使用相同的引用。每当依赖项数组中的变量之一发生变化时,该引用就会发生变化。如您所知,React尝试通过观察某些变量值的变化来最小化重新渲染过程,然后它决定重新渲染,而不取决于这些变量的旧值和新值。因此, 的基本用法useCallback是同等地保留旧值和新值。

我将尝试通过在我们必须使用的情况下给出一些示例来进一步演示它useCalback

  • 示例 1:当函数是useEffect.
function Component(){
  const [state, setState] = useState()
  
  // Should use `useCallback`
  function handleChange(input){
    setState(...)
  }

  useEffect(()=>{
    handleChange(...)
  },[handleChange])

  return ...
}
Run Code Online (Sandbox Code Playgroud)
  • 示例 2:当函数被传递给子组件之一时。特别是当它被调用时useEffect,它会导致无限循环。
function Parent(){
  const [state, setState] = useState()
  
  function handleChange(input){
    setState(...)
  }

  return <Child onChange={handleChange} />
}

function Child({onChange}){
  const [state, setState] = useState()
  
  useEffect(()=>{
    onChange(...)
  },[onChange])

  return "Child"
}
Run Code Online (Sandbox Code Playgroud)
  • 示例 3:当您使用React Context保存状态并仅返回状态设置器函数时,您需要该函数的使用者context不要在每次状态更新时重新渲染,因为这可能会损害性能。
const Context = React.createContext();

function ContextProvider({children}){
  const [state, setState] = useState([]);
  
  // Should use `useCallback`
  const addToState = (input) => {
    setState(prev => [...prev, input]);
  }

  // Should use `useCallback`
  const removeFromState = (input) => {
    setState(prev => prev.filter(elem => elem.id !== input.id));
  }

  // Should use `useCallback` with empty []
  const getState = () => {
    return state;
  }

  const contextValue= React.useMemo(
    () => ({ addToState , removeFromState , getState}),
    [addToState , removeFromState , getState]
  );

  // if we used `useCallback`, our contextValue will never change, and all the subscribers will not re-render
  <Context.Provider value={contextValue}>
    {children}
  </Context.Provider>
}
Run Code Online (Sandbox Code Playgroud)

示例 4:如果您订阅了观察者、计时器、文档事件,并且需要在组件卸载或由于任何其他原因时取消订阅。因此我们需要访问相同的引用来取消订阅。

function Component(){

  // should use `useCallback`
  const handler = () => {...}
  
  useEffect(() => {
    element.addEventListener(eventType, handler)
    return () => element.removeEventListener(eventType, handler)
  }, [eventType, element])


  return ...
}
Run Code Online (Sandbox Code Playgroud)

就是这样,您也可以在多种情况下使用它,但我希望这些示例演示了背后的主要思想useCallback并且永远记住,如果重新渲染的成本可以忽略不计,则不需要使用它。