前言
React中最让人畅谈的就是其带来的灵活性,可以说写起来非常的舒服。但是也就是它的灵活性太强,往往让我们忽略了很多细节的地方,而就是这些细节的东西能进行优化,减小我们的性能开销。可以说刚学React和工作几年后写React的代码真的是完全不一样。
废话不多说了,今天带大家来了解useCallback有哪些细节之处可以让我们做进一步的性能优化。
如果你不太了解useCallback,可以看看我这篇文章正确使用useCallback的姿势
没做优化前的代码
简单的一个结构,父子组件,子组件接收父组件触发数字改变的函数。
父组件
import React, { memo, useState } from "react";
import Son from "../commponet/Son";
const Optimize = memo(() => {
console.log("Optimize 组件重新渲染了");
const [count, setcount] = useState(10);
const [isshow, setisshow] = useState(true);
// 未优化
const addNum = () => {
setcount(count + 10);
};
const changeShow = () => {
setisshow(!isshow);
};
return (
<div>
<h2>Optimize</h2>
<h3>
count:{count}---isshow:{isshow.toString()}
</h3>
<button onClick={(e) => addNum()}>add+10</button>
<button onClick={(e) => changeShow()}>changeshow</button>
<Son addNum={addNum} />
</div>
);
});
export default Optimize;
子组件
import React, { memo } from "react";
interface Props {
count?: number;
addNum: Function;
}
const Son = memo((props: Props) => {
const { addNum } = props;
console.log("Son 组件重新渲染了");
return (
<div>
<h2>Son</h2>
<button onClick={(e) => addNum()}>click</button>
</div>
);
});
export default Son;
测试—结论
三个按钮都点击一遍,发现
原因不用多说了吧,看过我之前那篇文章的都懂,下面进行进一步优化。
第一次优化
使用useCallback来进行优化
父组件只贴一些关键代码
子组件代码未改变,这里就不贴了
// 第一次优化
const addNum = useCallback(() => {
setcount(count + 10);
}, [count]);
const changeShow = useCallback(() => {
setisshow(!isshow);
}, [isshow]);
测试—结论
前两次点击都是触发 addNum 最后一次点击触发 changeShow
一般优化都只是到此为止,可是我们想想,子组件中并未用到我们的count 只是接收了addNum函数,如果我们能只让父组件进行更新子组件不更新不才是最理想的状态吗?
最终优化
首先要搞明白为什么子组件会更新?那是因为每次count更新时,重新生成一个新函数,引用地址发生了变化,子组件发现前后;两次地址不一样所以进行了更新。那能不能让其始终用最开始的函数地址呢?
const changeShow = useCallback(() => {
setisshow(!isshow);
}, [isshow]);
// 第二次优化
const countRef = useRef(count);
countRef.current = count;
const addNum = useCallback(() => {
setcount(countRef.current + 10);
}, []);
使用空数组代表我们一直不会改变函数地址,用的是最开始的那个函数
那如何修改值呢?这里巧妙用useRef来帮助我们记录每次更新的值
为什么使用useRef? 因为useRef是贯穿整个生命周期的,每次render重新执行时useRef都是同一个对象。不信?那我们测试测试呗。
import React, { memo, useCallback, useRef, useState } from "react";
import Son from "../commponet/Son";
let obj: any = null;
const Optimize = memo(() => {
const aaa = useRef();
console.log(obj === aaa);
obj = aaa;
console.log("Optimize 组件重新渲染了");
const [count, setcount] = useState(10);
const [isshow, setisshow] = useState(true);
// 未优化
const addNum = () => {
setcount(count + 10);
};
const changeShow = () => {
setisshow(!isshow);
};
return (
<div>
<h2>Optimize</h2>
<h3>
count:{count}---isshow:{isshow.toString()}
</h3>
<button onClick={(e) => addNum()}>add+10</button>
<button onClick={(e) => changeShow()}>changeshow</button>
</div>
);
});
export default Optimize;
可以看到确实是同一个对象的,如果我们采用之前的count 或者自己定义一个新对象就会陷入闭包陷阱了!
它们都会保存最开始的值的引用,后续render更新的是新地址,不会被其引用,感兴趣的可以去测试测试,这里就不测试了。
// 用普通对象和之前值是否可行?闭包陷阱
const countRef = { current: count };
countRef.current = count;
const addNum = useCallback(() => {
// setcount(countRef.current + 10);
setcount(count + 10);
}, []);
测试—结论
三个按钮都点击了一次,发现都只是父组件更新了
总结
这里并不是说所有的 useCallback 都要进行这样处理,其实是没必要的。我们要根据实际情况来分析,就比如我们上面这个例子那是建立在子组件没有依赖父组件的count数据,所以我们可以选择只渲染父组件就可以,若是子组件也用到了count数据则推荐使用第一次优化即可。
一键三连,关注不迷路!