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

23

我已经阅读了一些关于何时使用和何时不使用useCallbackuseMemo的文章,但我大多数情况下看到的都是非常牵强附会的代码。我正在查看公司的一段代码,发现有人这样做:

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]);

这是如何调用 onUploadPress 的:

<TouchableOpacity
   style={styles.retakeButton}
   onPress={onUploadPress}
>

您认为这样叫正确吗?根据我对这些文章的理解,这看起来是不正确的。请问什么情况下需要使用useCallback函数?能否用更通俗易懂的语言解释一下useCallback
我阅读的文章:何时使用useMemo和useCallback

我没有给出完整的答案,因为我的理解还不够深入,但我同意你的观点,似乎在你的例子中并不需要使用useCallback()。 - Ben Wheeler
3个回答

22

useCallback接受一个函数作为第一个参数,并返回其记忆化版本(根据其内存位置而非内部计算)。这意味着返回的函数在组件重新渲染时不会在新的内存引用上重新创建,而组件内部的普通函数则会。

如果useCallback的依赖数组(第二个参数)中的变量发生变化,返回的函数将在新的内存引用上重新创建。

那么,为什么要费心使用这个功能呢?嗯,每当组件内部函数的正常行为对您有问题时,这是值得的。

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

useEffect的回调函数在首次渲染时被调用,并且每当其依赖数组中的变量之一发生变化时也会被调用。由于通常情况下,每次渲染都会创建一个新版本的该函数,因此回调函数可能会被无限调用。因此,使用useCallback来进行记忆。

通过使用memo进行记忆的组件只有在其stateprops发生变化时重新渲染,而不是因为其父组件重新渲染。由于通常情况下,当父组件重新渲染时,传递给子组件的函数作为props会创建一个新引用,因此子组件会重新渲染。因此,使用useCallback来进行记忆。

为了说明问题,我创建了下面这个可工作的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")
);
<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>

知道记忆化不是免费的,所以做错了更糟糕。在你的情况下,对于onUploadPress使用useCallback是浪费的,因为依赖数组中有一个非记忆化函数pickPhotoFromLibrary。另外,如果TouchableOpacity没有使用memo进行记忆化,也是一种浪费,但我不确定是否已经使用了memo。
顺便提一下,还有一个名为useMemo的方法,它的行为和用法与useCallback类似,用于记忆化非函数但被引用的值,例如对象和数组,出于相同的原因,或者用于记忆化任何重复渲染之间不想重复计算的结果。
了解React渲染过程的深入资源,可以帮助你知道何时进行记忆化以及如何正确地进行记忆化:React Render。

我觉得这个例子不太有用——为什么要定义一个函数,然后再创建一个依赖于它的useEffect()呢? - Ben Wheeler

15

简单来说,useCallback 用于保存函数引用,在组件渲染之外的某个地方,以便我们可以再次使用相同的引用。只要依赖数组中的变量之一发生更改,该引用就会更改。

正如您所知道的,React 通过监视某些变量的值的变化来尝试最小化重新呈现过程,然后根据这些变量的旧值和新值决定是否重新呈现。

因此,useCallback 的基本用法是平等地保存旧值和新值。

我将通过提供一些示例来演示在哪些情况下必须使用 useCallback

  • 示例1:当函数是 useEffect 的依赖数组之一时。

function Component(){
  const [state, setState] = useState()
  
  // Should use `useCallback`
  function handleChange(input){
    setState(...)
  }

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

  return ...
}
  • 示例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"
}
  • 示例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>
}
如果您已经订阅了观察者、计时器、文档事件,并且需要在组件卸载或其他任何原因下取消订阅,则需要访问相同的引用以进行取消订阅。
function Component(){

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


  return ...
}

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


我不明白第一个例子。为什么你要使用 useEffect,以至于每当 handleChange 函数的定义发生变化时,就调用它?实际上,并没有需要处理的变化,对吧? - Ben Wheeler
@BenWheeler 当然,你说得对,在这个例子中没有意义。但在现实世界中,你可能会通过多种情况遇到这种情况,例如: 当你正在监听另一个事件并希望通过调用此函数来做出反应时,你必须在effect回调中调用此函数。 一种可能的解决方案是将函数体移到useEffect回调中,但如果其他方法也在调用此函数呢? 另一种可能的解决方案是将函数移出React组件本身,但如果此函数需要读取/更新本地状态呢? - Ahmed Tarek

-1

useCallback 告诉 React 这个函数在每次渲染中不会改变,只有当它的依赖项发生变化时才会改变(我们必须传递一个依赖项数组,在 useEffect 中,您可以选择不传递)。它是在 useEffect 之后添加的,以处理与 useEffect 相关的错误,如其他答案中所解释的。

除了其他响应之外,它还可以防止组件重新渲染。假设您将事件处理程序作为 prop 传递给子组件,

const ParentComponent = ()=> {
   const eventHandler = ()=> { }
   return <ChildComponent onClick={eventHandler}/>
}

每次重新渲染ParentComponents时,都会创建一个新的eventHandler,因此您正在向ChildComponent传递一个新的prop。由于其prop将更改,ChildComponent将重新渲染。如果您使用useCallback包装eventHandler,则子组件将不会重新渲染。

或者,如果您有一个按钮元素的单击处理程序,如果您使用useCallback包装单击处理程序,则该按钮元素将不会重新渲染。如果没有,将为该元素创建一个新的单击处理程序。当React检测到JSX中的任何更改时,它将重新渲染该部分。


1
如果您使用 useCallback 包装 eventHandler,子组件不会重新渲染。这是不正确的,如果 ParentComponent 重新渲染,ChildComponent 将重新渲染。也许您的意思是,如果 ChildComponent 的 useEffect 依赖于 onClick prop,它将不会在重新渲染期间重新运行,因为它的引用在 ParentComponent 的重新渲染期间保持不变,即重新渲染期间“存活”。 - Aleksandar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接