前言
本篇文章尝试解释useCallback的基本用法。 在了解useCallback之前,你需要知道JS是怎么处理函数相等性检查。
function fn() {
return (i)=>i;
}
const f1 = fn();
const f2 = fn();
//返回false
console.log(f1 === f2);
什么是useCallBack
useCallback 是 React 中的一个钩子函数,它主要用来缓存回调函数。
useCallback 接受两个参数:第一个参数是回调函数,第二个参数是依赖数组。当依赖数组中的任意一个值发生变化时,useCallback 会重新创建一个新的回调函数。如果依赖数组为空,则回调函数永远不会被更新。
Demo展示代码链接🔗: codesandbox.io/s/awesome-b…
展示的页面如下:
没有使用useCallback
注意:下面的例子仅仅帮助理解useCallback的用法,并不是使用useCallback的正确场景
首先 我们先写一个不使用useCallback的组件。
//App.jsx 作为父组件
import ChildComponent from "./ChildComponent";
import { useState, useCallback } from "react";
export default function App() {
const [logic, setLogic] = useState(false);
const [count, setCount] = useState(0);
const handleClickCount = () => {
setCount(count + 1);
};
//每次App重新渲染handleCallback都是不同的函数
const handleCallback() = () => {
setLogic((pre) => !pre);
}
return (
<div>
<p>数值: {count}</p>
<p>布尔值: {logic.toString()}</p>
<button onClick={handleClickCount}>增加数值</button>
<ChildComponent callback={handleCallback} />
</div>
);
}
然后我们再来看看 子组件 ChildComponent里面是什么样子的。
//子组件
import { useEffect, memo } from "react";
export default function ChildComponent({ callback }) {
//使用useEffect来监听callback是否发生变化, ”child“ 会被打印出来
useEffect(() => {
console.log("in child useEffect");
}, [callback]);
//这里我们观察子组件是否再次渲染,"render child" 也会被打印出来
console.log("render child");
return <button onClick={callback}>改变布尔值</button>;
}
操作观察
如果我们点击增加数值按钮的时候。”render child“ 和 ”child“ 都会被打印出来。
- ”in child useEffect" 被打印,说明ChildComponent props 接收到的callback每次被重新创建。
- "render child“ 被打印说明每次点击增加数值按钮的时候,ChildComponent被重新渲染。
使用useCallback来缓存handleCallback可以防止handleCallback每次被重新创建,也就是解决第1条。
修改代码如下
给App.jsx里面加上一个新的函数handleCallbackWithUseCallback。
export default function App() {
const [logic, setLogic] = useState(false);
const [count, setCount] = useState(0);
const handleClickCount = () => {
setCount(count + 1);
};
const handleCallback = ()=> {
setLogic((pre) => !pre);
}
//增加的部分
const handleCallbackWithUseCallback = useCallback(handleCallback, []);
return (
<div>
<p>数值: {count}</p>
<p>布尔值: {logic.toString()}</p>
<button onClick={handleClickCount}>增加数值</button>
{/* 修改部分 */}
<ChildComponent callback={handleCallbackWithUseCallback} />
</div>
);
}
操作观察
- ”in child useEffect" 没有被打印出来
- "render child“ 仍然被打印。
useCallback只会缓存回调函数handleCallback。点击增加数值的按钮的时候,父组件中的count变量发生了改变,App组件重新渲染,ChildComponent组件也就随着被渲染了。可是 ChildComponent组件和App的count变量并没有关系,count的改变,我们不希望引发ChildComponent组件的重新渲染。如果要解决组件重新渲染的问题的话,单用useCallback是没有用的,还需要加上React.memo。
useCallback和React.memo同时使用代码。
我们可以新建一个新的文件MemoChildComponent
import react from "react";
import ChildComponent from "./ChildComponent";
export default react.memo(ChildComponent);
把这个MemoChildComponent引入到App中, 并配合useCallback使用。
export default function App() {
const [logic, setLogic] = useState(false);
const [count, setCount] = useState(0);
const handleClickCount = () => {
setCount(count + 1);
};
const handleCallback = ()=> {
setLogic((pre) => !pre);
}
const handleCallbackWithUseCallback = useCallback(() => {
setLogic((pre) => !pre);
}, []);
return (
<div>
<p>数值: {count}</p>
<p>布尔值: {logic.toString()}</p>
<button onClick={handleClickCount}>增加数值</button>
<MemoChildComponent callback={handleCallbackWithUseCallback} />
</div>
);
}
操作观察
这个时候 如果我们点击增加数值的按钮的时候。我们发现 ”render child“ 和 ”in child useEffect“都不会被打印。Count的变化对MemoChildComponent没有影响。
错误使用useCallback例子
虽然useCallback是一种优化的手段。但是这种优化,也会产生调用useCallback所用的资源消耗,同时加上useCallback也会增加代码的复杂度。对于简单的组件来说,滥用useCallback不仅不会优化,反而会适得其反的。本文中使用的例子也是一个不正确的应用场景,但是可以方便对useCallback使用的理解。
错误例子
import { useCallback } from 'react';
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child onClick={handleClick} />;
}
function Child ({ onClick }) {
return <button onClick={onClick}>按钮</button>;
}
总结
- useCallback在它的依赖不变的状况下,总是返回相同的函数
- useCallback可以和React.memo 结合在一起使用来避免不必要的渲染。