React hook 之 useCallback

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。简单的说就是将一个函数进行缓存,避免它在多次渲染中反复执行。

语法

const cachedFn = useCallback(fn, dependencies);

参数

  • fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。React 将会在初次渲染而非调用时返回该函数。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。

  • dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。React 使用 Object.is 比较每一个依赖和它的之前的值。

返回值

在初次渲染时,useCallback 返回你已经传入的 fn 函数

在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn。

注意事项

useCallback 是一个 Hook,所以应该在 组件的顶层 或自定义 Hook 中调用。你不应在循环或者条件语句中调用它。如果你需要这样做,请新建一个组件,并将 state 移入其中。

除非有特定的理由,React 将不会丢弃已缓存的函数。
例如,在开发中,当编辑组件文件时,React 会丢弃缓存。在生产和开发环境中,如果你的组件在初次挂载中暂停,React 将会丢弃缓存。

在未来,React 可能会增加更多利用了丢弃缓存机制的特性。
例如,如果 React 未来内置了对虚拟列表的支持,那么在滚动超出虚拟化表视口的项目时,抛弃缓存是有意义的。如果你依赖 useCallback 作为一个性能优化途径,那么这些对你会有帮助。否则请考虑使用 state 变量 或 ref。

使用场景

跳过组件的重新渲染

当你优化渲染性能的时候,有时需要缓存传递给子组件的函数。

为了缓存组件中多次渲染的函数,你需要将其定义在 useCallback Hook 中:

import { useCallback } from "react"; function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback( (orderDetails) => { post("/product/" + productId + "/buy", { referrer, orderDetails, }); }, [productId, referrer] ); //.... }

你需要传递两个参数给 useCallback:

在多次渲染中需要缓存的函数 函数内部需要使用到的所有组件内部值的 依赖列表。 初次渲染时,在 useCallback 处接收的 返回函数 将会是已经传入的函数。

在之后的渲染中,React 将会使用 Object.is 把 当前的依赖 和已传入之前的依赖进行比较。如果没有任何依赖改变,useCallback 将会返回与之前一样的函数。否则 useCallback 将返回 此次 渲染中传递的函数。

简而言之,useCallback 在多次渲染中缓存一个函数,直至这个函数的依赖发生改变。

从 useCallback 中更新 state

有时,你可能在 useCallback 中基于之前的 state 来更新 state。 下面的 handleAddTodo 函数将 todos 指定为依赖项,因为它会从中计算下一个 todos:

function TodoList() { const [todos, setTodos] = useState([]); const handleAddTodo = useCallback( (text) => { const newTodo = { id: nextId++, text }; setTodos([...todos, newTodo]); }, [todos] ); //.... }

我们期望记忆化函数具有尽可能少的依赖,当你读取 state 只是为了计算下一个 state 时,你可以通过传递 updater function 以移除该依赖:

function TodoList() { const [todos, setTodos] = useState([]); const handleAddTodo = useCallback((text) => { const newTodo = { id: nextId++, text }; setTodos((todos) => [...todos, newTodo]); }, []); // ✅ 不需要 todos 依赖项 //.... }

在这里,并不是将 todos 作为依赖项并在内部读取它,而是传递一个关于 如何 更新 state 的指示器 (todos => [...todos, newTodo]) 给 React。

防止频繁触发 Effect

有时,你想要在 Effect 内部调用函数,这会产生一个问题,每一个响应值都必须声明为 Effect 的依赖,如果内部调用的函数作为依赖传入,则需要把函数包裹在 useCallback 中

function ChatRoom({ roomId }) { const [message, setMessage] = useState(""); const createOptions = useCallback(() => { return { serverUrl: "https://localhost:1234", roomId: roomId, }; }, [roomId]); // ✅ 仅当 roomId 更改时更改 useEffect(() => { const options = createOptions(); const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, [createOptions]); // ✅ 仅当 createOptions 更改时更改 // ... }

这将确保如果 roomId 相同,createOptions 在多次渲染中会是同一个函数。但是,最好消除对函数依赖项的需求。将你的函数移入 Effect 内部:

function ChatRoom({ roomId }) { const [message, setMessage] = useState(""); useEffect(() => { function createOptions() { // ✅ 无需使用回调或函数依赖! return { serverUrl: "https://localhost:1234", roomId: roomId, }; } const options = createOptions(); const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, [roomId]); // ✅ 仅当 roomId 更改时更改 // ... }

优化自定义 Hook

如果你正在编写一个 自定义 Hook,建议将它返回的任何函数包裹在 useCallback 中:

function useRouter() { const { dispatch } = useContext(RouterStateContext); const navigate = useCallback( (url) => { dispatch({ type: "navigate", url }); }, [dispatch] ); const goBack = useCallback(() => { dispatch({ type: "back" }); }, [dispatch]); return { navigate, goBack, }; }

常见问题

组件每一次渲染时, useCallback 都返回了完全不同的函数

确保你已经将依赖数组指定为第二个参数!

如果你忘记使用依赖数组,useCallback 每一次都将返回一个新的函数: