编辑
2024-01-07
React
00
请注意,本文编写于 201 天前,最后修改于 201 天前,其中某些信息可能已经过时。

目录

基本使用
实现原理
最佳实践
总结

在本文中,我们将重点介绍一个React常用的内置Hook,即useCallback。useCallback可以让我们缓存函数,避免因为函数引用的变化而导致不必要的子组件重渲染。我们讲解它的基本使用、实现原理、与useMemo的区别、最佳实践等。

基本使用

useCallback是一个React Hook,所以我们只能在函数式组件或者自定义Hook中调用它,不能在循环或者条件语句中调用它。useCallback的基本语法如下:

js
const cachedFn = useCallback(fn, dependencies);

useCallback接受两个参数,分别是:

  • fn:一个函数,它可以接受任意的参数,返回任意的值。这个函数是我们想要缓存的函数,它的定义不应该依赖于组件的状态或者属性,否则可能会导致缓存失效或者闭包陷阱。
  • dependencies:一个数组,它包含了所有fn中引用的依赖项,例如状态、属性、变量或者函数等。这些依赖项必须是可比较的,即可以用Object.is来判断它们是否相等。如果我们忽略了某些依赖项,或者提供了一个空数组,那么useCallback将无法正确地更新缓存的函数,可能会导致意想不到的结果。

useCallback的返回值是一个函数,它是fn的一个缓存版本。

在组件的初始渲染时,useCallback会返回fn本身。在后续的渲染中,useCallback会根据依赖项的变化来决定是否返回上一次的缓存函数,还是返回当前的fn

如果依赖项没有变化,useCallback会返回上一次的缓存函数,这样可以保证函数的引用不变,从而避免触发子组件的重渲染。如果依赖项有变化,useCallback会返回当前的fn,并将其缓存起来,以备下次使用。

下面是一个简单的例子,演示了useCallback的基本用法:

js
import React, { useState, useCallback } from "react"; // 一个子组件,接受一个函数作为属性 function Child({ onClick }) { console.log("Child rendered"); return <button onClick={onClick}>Click Me</button>; } // 一个父组件,使用useCallback来缓存一个函数 function Parent() { const [count, setCount] = useState(0); // 使用useCallback来创建一个缓存的函数,该函数会更新count状态 // 该函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化 const handleClick = useCallback(() => { setCount((prevCount) => prevCount + 1); }, [setCount]); return ( <div> <p>Count: {count}</p> {/* 将缓存的函数传递给子组件 */} <Child onClick={handleClick} /> </div> ); }

在这个例子中,父组件使用useState来创建一个count状态,然后使用useCallback来创建一个缓存的函数,该函数会更新count状态。注意,这个函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化,所以我们可以放心地将它作为依赖项。然后,父组件将这个缓存的函数传递给子组件,子组件接受这个函数作为属性,并在按钮上绑定这个函数。

当我们运行这个例子时,我们可以看到,每当我们点击按钮时,count状态会增加,父组件会重新渲染,但是子组件不会重新渲染,因为它接收的函数的引用没有变化。这样,我们就利用useCallback来优化了子组件的性能,避免了不必要的重渲染。

实现原理

useCallback的实现原理其实很简单,它就是利用了闭包和数组来存储和比较函数和依赖项。我们可以用以下的伪代码来模拟useCallback的实现过程:

js
// 定义一个全局的缓存对象,用来存储函数和依赖项 const cache = { fn: null, // 存储函数 dependencies: [], // 存储依赖项 }; // 定义一个useCallback函数,接受函数和依赖项作为参数,返回一个缓存的函数 function useCallback(fn, dependencies) { // 如果缓存对象中没有存储函数,或者依赖项的长度不一致,或者依赖项有变化 if ( cache.fn === null || cache.dependencies.length !== dependencies.length || dependencies.some((dep, i) => !Object.is(dep, cache.dependencies[i])) ) { // 将当前的函数和依赖项存储到缓存对象中 cache.fn = fn; cache.dependencies = dependencies; } // 返回缓存对象中的函数 return cache.fn; }

从上面的伪代码中,我们可以看到,useCallback其实就是通过一个全局的缓存对象来存储和返回函数,同时通过比较依赖项的长度和值来判断是否需要更新缓存对象。当然,这只是一个简化的版本,实际的useCallback可能会更复杂一些,但是基本的思路是一样的。

有些人可能会问,useCallbackuseMemo有什么区别呢?它们不都是用来缓存值的吗?

其实,useCallbackuseMemo的区别主要在于它们缓存的值的类型。useCallback缓存的是函数,而useMemo缓存的是任意的值,包括函数、对象、数组等。

useCallbackuseMemo的用法也有一些不同,useCallback返回的是一个函数,我们可以直接调用它,而useMemo返回的是一个值,我们需要将它赋值给一个变量或者常量。useCallbackuseMemo的语法如下:

js
// useCallback的语法 const cachedFn = useCallback(fn, dependencies); // useMemo的语法 const cachedValue = useMemo(() => value, dependencies);

从上面的语法中,我们可以看到,useCallback接受一个函数作为第一个参数,而useMemo接受一个函数的返回值作为第一个参数。

这意味着,useCallback只会在依赖项变化时执行一次函数,而useMemo会在每次渲染时都执行一次函数,只是在依赖项变化时才会更新缓存的值。

因此,useCallback适合用来缓存那些不需要立即执行的函数,而useMemo适合用来缓存那些需要立即执行的值。

下面是一个例子,演示了useCallbackuseMemo的区别:

js
import React, { useState, useCallback, useMemo } from "react"; // 一个子组件,接受一个函数和一个值作为属性 function Child({ onClick, value }) { console.log("Child rendered"); return ( <div> <p>Value: {value}</p> <button onClick={onClick}>Click Me</button> </div> ); } // 一个父组件,使用useCallback和useMemo来缓存一个函数和一个值 function Parent() { const [count, setCount] = useState(0); // 使用useCallback来创建一个缓存的函数,该函数会更新count状态 // 该函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化 const handleClick = useCallback(() => { setCount((prevCount) => prevCount + 1); }, [setCount]); // 使用useMemo来创建一个缓存的值,该值是count的平方 // 该值的依赖项是count,它是一个可变的状态,会随着渲染而变化 const squared = useMemo(() => { console.log("squared computed"); return count * count; }, [count]); return ( <div> <p>Count: {count}</p> {/* 将缓存的函数和值传递给子组件 */} <Child onClick={handleClick} value={squared} /> </div> ); }

在这个例子中,父组件使用useState来创建一个count状态,然后使用useCallback来创建一个缓存的函数,该函数会更新count状态。

注意,这个函数的依赖项是setCount,它是一个稳定的函数,不会随着渲染而变化,所以我们可以放心地将它作为依赖项。

然后,父组件使用useMemo来创建一个缓存的值,该值是count的平方。注意,这个值的依赖项是count,它是一个可变的状态,会随着渲染而变化,所以我们需要将它作为依赖项。然后,父组件将这个缓存的函数和值传递给子组件,子组件接受这个函数和值作为属性,并在按钮上绑定这个函数,以及显示这个值。

当我们运行这个例子时,我们可以看到,每当我们点击按钮时,count状态会增加,父组件会重新渲染,子组件也会重新渲染,因为它接收的值的引用发生了变化。

同时,我们可以看到控制台的输出,每当我们点击按钮时,useMemo会重新计算squared的值,并打印出"squared computed",而useCallback不会重新执行函数,只是返回上一次的缓存函数。这样,我们就可以看出useCallbackuseMemo的区别和联系。

最佳实践

useCallback是一个有用的Hook,但是它也有一些需要注意的地方。在使用useCallback时,我们应该遵循以下的一些最佳实践:

  • 不要滥用useCallback。useCallback并不是一个万能的性能优化工具,它也有一定的开销,例如创建和比较依赖项数组,以及存储和返回缓存函数。如果我们不需要缓存函数,或者缓存函数的收益小于开销,那么使用useCallback反而会降低性能,而不是提高性能。因此,我们应该在必要的时候才使用useCallback,例如当我们需要将函数作为属性传递给子组件,或者当我们需要将函数作为依赖项传递给其他Hook时。
  • 不要忽略依赖项。useCallback的第二个参数是一个非常重要的参数,它决定了缓存函数的有效性和正确性。如果我们忽略了某些依赖项,或者提供了一个空数组,那么useCallback将无法正确地更新缓存函数,可能会导致缓存函数引用了过期的状态或者属性,或者触发了无限循环或者内存泄漏等问题。因此,我们应该尽量完整地提供所有的依赖项,或者使用一些工具,例如eslint-plugin-react-hooks,来帮助我们检查和修复依赖项的问题。
  • 不要在缓存函数中定义状态或者属性。useCallback的第一个参数是一个函数,它可以接受任意的参数,返回任意的值。但是,这个函数的定义不应该依赖于组件的状态或者属性,否则可能会导致缓存失效或者闭包陷阱。例如,下面的代码就是一个错误的用法:
js
import React, { useState, useCallback } from "react"; function Counter() { const [count, setCount] = useState(0); // 错误的用法:在缓存函数中定义了一个状态 const increment = useCallback(() => { // 这里的count是一个闭包变量,它只会在初始渲染时被捕获,后续的渲染不会更新它 // 这会导致increment函数总是返回1,而不是正确的count + 1 const [count, setCount] = useState(0); return count + 1; }, []); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(increment)}>Increment</button> </div> ); }

在这个例子中,我们在缓存函数中定义了一个状态,然后将这个缓存函数作为setCount的参数。

这是一个错误的用法,因为这个缓存函数只会在初始渲染时被创建,它捕获了当时的count值,后续的渲染不会更新这个值,导致increment函数总是返回1,而不是正确的count + 1

这就是一个典型的闭包陷阱,它会让我们的状态不同步,造成逻辑错误。为了避免这个问题,我们应该将状态或者属性作为缓存函数的参数,而不是在缓存函数中定义它们。例如,下面的代码就是一个正确的用法:

js
import React, { useState, useCallback } from "react"; function Counter() { const [count, setCount] = useState(0); // 正确的用法:将状态作为缓存函数的参数 const increment = useCallback((count) => { // 这里的count是一个参数,它会随着setCount的调用而更新,保持同步 // 这会导致increment函数返回正确的count + 1 return count + 1; }, []); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount((prevCount) => increment(prevCount))}> Increment </button> </div> ); }

在这个例子中,我们将count作为缓存函数的参数,然后将这个缓存函数作为setCount的参数。这是一个正确的用法,因为这个缓存函数不会依赖于任何状态或者属性,它只会根据传入的参数来返回一个值,这样就避免了闭包陷阱,保证了状态的同步,实现了逻辑的正确。

总结

useCallback是一个可以让我们缓存函数的Hook,它可以帮助我们优化性能,避免不必要的子组件重渲染。

useCallback的实现原理其实很简单,它就是利用了闭包和数组来存储和比较函数和依赖项。

useCallbackuseMemo的区别主要在于它们缓存的值的类型。useCallback缓存的是函数,而useMemo缓存的是任意的值,包括函数、对象、数组等。

不要滥用useCallback,它也有开销。不要忽略依赖项,它决定了缓存函数的正确性。不要在缓存函数中定义状态或者属性,它会导致闭包陷阱。

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:CreatorRay

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!