React中的一些Hooks的理解
注意:react18 的话可以取消严格模式,再运行以下代码否则会出现状态改变一次组件重新渲染两次的问题(好像是函数式组件遇到的问题 具体分析可以看 https://juejin.cn/post/7092696529105846303#heading-2)
参考:https://juejin.cn/post/6844904093824073742#heading-5
这两个钩子一般用于性能的优化,而且有时会用于搭配使用
一、useMemo 的使用
useMemo
- 应用场景:一般应用于优化子组件,减少子组件不必要的重render
- 作用:当父组件的数据发生改变的时候,但是这些数据的改变可能与子组件无关,为了避免子组件不必要的重新渲染可以在子组件上包裹useMemo
下面看一下示例
首先是不使用
useMemo
早成子组件不必要渲染的
父组件定义
import "./styles.css";
import Info from "./Info";
import { useState } from "react";
export default function App() {
const [count1, setCount1] = useState(0);
const [s, changes] = useState(false);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<button
onClick={() => {
changes(!s);
}}
>
改变S
</button>
</div>
<button
onClick={() => {
setCount1(count1 + 1);
}}
>
+1
</button>
<Info info={count1} name={"第一个"} />
</div>
);
}
子组件定义
const Info = ({ name, info }) => {
console.log(name + " -> 我被重新渲染了");
return <div>我是子组件:{info}</div>;
};
export default Info;
页面效果:
在这个案例里面我们我们定义了按钮
改变s
去改变s
的数据,但是s的值与子组件的状态无关。当我们点击按钮改变S
的时候子组件就会重新渲染,结果如下图,我们没有点击 +1 按钮,而子组件就已经重新渲染了8次,这显然是不合理的,因此我们修改一下子组件的代码可以解决这种问题。
使用memo 的子组件
const Info = ({ name, info }) => {
console.log(name + " -> 我被重新渲染了");
return <div>我是子组件:{info}</div>;
};
export default Info;
此时我们再重复点击按钮
改变S
则不会出现子组件重新渲染的问题,因为使用memo包裹后当父组件状态改变并不会直接触发子组件的重新渲染,而是会判断前后的props是否发生改变,若发生改变才触发重新渲染,因此当props中含有Object 的类型的时候可能会失效。
失效的原因:
const a = {};
const b = {};
a === b; // false
二、useCallback 的使用
1.引入库
useCallback
用于对数组进行缓存,用于缓存一个函数。
应用场景:避免函数的重复创建、当父组件给子组件传递函数的时候和usememo搭配使用避免子组件重复渲染
修改上例代码:
父组件
import "./styles.css";
import Info from "./Info";
import { useCallback, useState } from "react";
export default function App() {
const [count1, setCount1] = useState(0);
const [s, changes] = useState(false);
const getCount1 = () => {
return count1;
};
const callbackGetCount1 = useCallback(() => {
return count1;
}, [count1]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<button
onClick={() => {
changes(!s);
}}
>
改变S
</button>
</div>
<button
onClick={() => {
setCount1(count1 + 1);
}}
>
+1
</button>
<Info info={getCount1} name={"第一个"} />
<Info info={callbackGetCount1} name={"第二个"} />
</div>
);
}
子组件
import react from "react";
const Info = ({ name, info }) => {
console.log(name + " -> 我被重新渲染了");
return (
<div>
我是{name}子组件:{info()}
</div>
);
};
export default react.memo(Info);
页面
重复点击改变S
按钮
当我们点击
改变S
按钮的时候发现虽然子组件都用useMemo包裹了,但是子组件1被重新渲染,而子组件2没有被重新渲染。这就是前面提到的useMemo 对props 的浅比较引起的,对于子组件1我们每次都会传给它一个新的getCount1
函数,虽然内容是一样的但是并不相等,如下证明两个函数不相等。
失效的原因:
const a = () => {};
const b = () => {};
a === b; // false
那么子组件2 为什么没有被重复渲染呢?这是因为我们给子组件2传递的函数是通过
useCallback
包裹过的callbackGetCount1
,它会对函数进行缓存,在这个案例中只有当count1(也就是useCallback 通过第二个参数中监听到的数据)改变的时候这个函数才会重新创建从而引起子组件的useMemo的失效。我们通过一个实例来验证这个说法。
只修改上述案例中的父组件的useCallback部分的代码为
// 修改前
// const callbackGetCount1 = useCallback(() => {
// return count1;
// }, [count1]);
// 修改后
const callbackGetCount1 = useCallback(() => {
return count1;
}, [s]);
这个时候当我们再去重复的点击按钮
改变S
发现子组件二也会跟着发生重新渲染。
useCallback
好像会对值进行深度拷贝,也就是缓存,这刚好和它接受第二个参数去监听数据变化想匹配。
额外实验
修改上述的父组件
import "./styles.css";
import Info from "./Info";
import { useCallback, useState } from "react";
export default function App() {
const [count1, setCount1] = useState(0);
const getCount1 = () => {
return count1;
};
const callbackGetCount1 = useCallback(() => {
console.log(count1);
setCount1(count1 + Math.random());
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<button
onClick={() => {
callbackGetCount1();
}}
>
触发callback函数
</button>
</div>
<button
onClick={() => {
setCount1(count1 + 1);
}}
>
+1
</button>
<Info info={getCount1} name={"第一个"} />
</div>
);
}
页面:
我们可以进行以下操作,比如重复点击
触发callback函数
这个按钮,发现控制台输出如下
发现count1
的值并没有改变,但是页面上的子组件中对应显示的count1
发生改变。也就是说我们在外面通过useState
创建的count1
和useCallback
中的count1
不是一个同一个东西,但是setCount1
却是一样的,所以我猜测这里应该是useCallback对传入的函数中的那些变量进行了一次值拷贝才出现这样的情况(还没没看过源码具体原理还不是很清楚).
三、useRef 的两种用法
1 、获取DOM元素的对象
import { useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
const myRef = useRef<any>(HTMLInputElement);
useEffect(() => {}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<input ref={myRef} />
<button
onClick={() => {
console.log(myRef);
console.log(myRef.current.value);
}}
>
输出
</button>
</div>
);
}
页面
我们通过
myRef
跟input 框绑定,方便我们从其他地方获取input框中的内容(而不是通过js获取dom元素的值)。
**点击输出
按钮控制台的结果
2 、缓存数值
直接生命存储数据的案例
import { useState } from "react";
import "./styles.css";
export default function App() {
const [uodate, setUpdate] = useState(false);
let num = 0;
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<input
onChange={(e) => {
setUpdate(!uodate);
}}
/>
<button
onClick={() => {
num++;
}}
>
点我+ num
</button>
<button
onClick={() => {
console.log(num);
}}
>
输出
</button>
</div>
);
}
页面
首先是关于页面内容的说明:
- 出入框搭配
useState
实现当输入内容变化的时候组件重新渲染点我 + num
按钮可以实现增加 num 的值输出
按钮则可以在控制台输出num 的值
实验的步骤:
- 连续点击2(没有要求,点了就行)次
点我 + num
按钮- 点击
输出
按钮- 向输入框中输入一个值
- 再次点击
输出
按钮- 观察控制台输出
控制台结果
在这个实验中,我们通过步骤三确定了num可以成功加上,但是由于给输入框输入值的时候触发了页面的刷新导致let num = 0;
语句重新执行,第二次点击输出按钮的时候输出的num 的值又变回了0
使用 useRef 存储数据的案例
import { useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [uodate, setUpdate] = useState(false);
const num = useRef(0);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<input
onChange={(e) => {
setUpdate(!uodate);
}}
/>
<button
onClick={() => {
num.current++;
}}
>
点我+ num
</button>
<button
onClick={() => {
console.log(num.current);
}}
>
输出
</button>
</div>
);
}
页面的布局和作用跟上面那个相同
首先是关于页面内容的说明:
- 出入框搭配
useState
实现当输入内容变化的时候组件重新渲染点我 + num
按钮可以实现增加 num 的值输出
按钮则可以在控制台输出num 的值
实验的步骤:
- 连续点击2(没有要求,点了就行)次
点我 + num
按钮- 点击
输出
按钮- 向输入框中输入一个值
- 再次点击
输出
按钮- 观察控制台输出
控制台结果
输出的结果符合预期
其实上面的还可以通过在函数外面var一个num变量也可以避免这种问题的出现,不过其实用哪种方式存储数据,还是要根据我们的场景,有时候我们可能就是需要那种每次刷新变量重新赋值的场景
总结
useMemo可以理解成缓存组件的props 的,只有组件的props变化才会触发组件重新渲染
useCallback可以理解成缓存函数的,只有当这个useCallback对应监听的数据发生改变的时候,才会重新创建这个函数。