在本文中,我们将重点介绍一个React常用的内置Hook,即useCallback。useCallback可以让我们缓存函数,避免因为函数引用的变化而导致不必要的子组件重渲染。我们讲解它的基本使用、实现原理、与useMemo的区别、最佳实践等。
公众号:Code程序人生,个人网站:https://creatorblog.cn
useCallback
是一个React Hook
,所以我们只能在函数式组件或者自定义Hook
中调用它,不能在循环或者条件语句中调用它。useCallback
的基本语法如下:
const cachedFn = useCallback(fn, dependencies);
useCallback
接受两个参数,分别是:
useCallback
的返回值是一个函数,它是fn
的一个缓存版本。
在组件的初始渲染时,useCallback
会返回fn
本身。在后续的渲染中,useCallback
会根据依赖项的变化来决定是否返回上一次的缓存函数,还是返回当前的fn
。
如果依赖项没有变化,useCallback
会返回上一次的缓存函数,这样可以保证函数的引用不变,从而避免触发子组件的重渲染。如果依赖项有变化,useCallback
会返回当前的fn
,并将其缓存起来,以备下次使用。
下面是一个简单的例子,演示了useCallback
的基本用法:
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
的实现过程:
// 定义一个全局的缓存对象,用来存储函数和依赖项
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
可能会更复杂一些,但是基本的思路是一样的。
有些人可能会问,useCallback
和useMemo
有什么区别呢?它们不都是用来缓存值的吗?
其实,useCallback
和useMemo
的区别主要在于它们缓存的值的类型。useCallback
缓存的是函数,而useMemo
缓存的是任意的值,包括函数、对象、数组等。
useCallback
和useMemo
的用法也有一些不同,useCallback
返回的是一个函数,我们可以直接调用它,而useMemo
返回的是一个值,我们需要将它赋值给一个变量或者常量。useCallback
和useMemo
的语法如下:
// useCallback的语法
const cachedFn = useCallback(fn, dependencies);
// useMemo的语法
const cachedValue = useMemo(() => value, dependencies);
从上面的语法中,我们可以看到,useCallback
接受一个函数作为第一个参数,而useMemo
接受一个函数的返回值作为第一个参数。
这意味着,useCallback
只会在依赖项变化时执行一次函数,而useMemo
会在每次渲染时都执行一次函数,只是在依赖项变化时才会更新缓存的值。
因此,useCallback
适合用来缓存那些不需要立即执行的函数,而useMemo
适合用来缓存那些需要立即执行的值。
下面是一个例子,演示了useCallback
和useMemo
的区别:
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
不会重新执行函数,只是返回上一次的缓存函数。这样,我们就可以看出useCallback
和useMemo
的区别和联系。
useCallback
是一个有用的Hook
,但是它也有一些需要注意的地方。在使用useCallback
时,我们应该遵循以下的一些最佳实践:
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
。
这就是一个典型的闭包陷阱,它会让我们的状态不同步,造成逻辑错误。为了避免这个问题,我们应该将状态或者属性作为缓存函数的参数,而不是在缓存函数中定义它们。例如,下面的代码就是一个正确的用法:
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
的实现原理其实很简单,它就是利用了闭包和数组来存储和比较函数和依赖项。
useCallback
和useMemo
的区别主要在于它们缓存的值的类型。useCallback
缓存的是函数,而useMemo
缓存的是任意的值,包括函数、对象、数组等。
不要滥用useCallback
,它也有开销。不要忽略依赖项,它决定了缓存函数的正确性。不要在缓存函数中定义状态或者属性,它会导致闭包陷阱。