1.memo - 组件缓存
1.1 问题
函数组件的更新的条件:state/ props/ context上下文/ 父组件更新子组件无脑更新(不论props或者state)
上面几个更新条件中,前三个还算合理,但是第四个-无脑更新就有点浪费性能了,而比较合理的父子组件更新方式是:父组件给子组件的props更新,子组件再更新才行。而memo的诞生就是为了实现这个功能的-- 即当父组件有props交互时,让子组件根据props更新触发渲染,而不是无脑渲染。
1.2 语法
使用memo-API包装函数组件即可
import React, { memo } from "react";
const ChildComponent = memo(function ChildComponent({ text }) {
console.log("ChildComponent rendered");
return <div>{text}</div>; }
);
function ParentComponent({ showChild }) {
return (
<div>
{showChild && <ChildComponent text="Hello, world!" />}
<button onClick={() => setShowChild(!showChild)}>Toggle child</button>
</div>
);
}
在这个例子中,我们创建了一个名为 ChildComponent 的组件,并使用 memo 高阶组件对其进行了优化。我们还创建了一个名为 ParentComponent 的组件,它可以切换 ChildComponent 的显示。当 ParentComponent 重新渲染时,ChildComponent 的属性没有发生变化,因此它不会重新渲染。
存在问题:
虽然使用memo包裹的组件可以实现只有父组件的props变化之后,子组件才更新渲染,但是因为react是浅比较,不保证props中的简单类型和复杂类型存储地址变化,因此出现了useMemo和useCallback。
1.3 对比pureComponent
pureComponent是class类组件对重新渲染的.在类组件中通过pureComponent继承来实现组件的避免重复渲染。
2.useMemo - 数据缓存
2.1 解决问题:同computed,避免重复计算
我们都知道react的渲染特性相当于每一次渲染都是一次快照,会生成新的state状态,然后根据这个新的state状态构建dom,交给GUI渲染线程进行style树构建、合成树、分成、绘制、合成绘制。那么就存在以下问题:
import React, { useState } from "react";
function App() {
const [likeCount, setLikeCount] = useState(2)
// 桃子的单价和价格
const [peach, setPeach] = useState({
num: 10,
unitPrice: 5
});
// 香蕉的单价和价格
const [banana, setBanana] = useState({
num: 10,
unitPrice: 10
});
// 2. 每次渲染时候立即计算:计算桃子总价
let price = 0
function count() {
console.log("价格重新计算了--");
price = (peach.num * peach.unitPrice)
}
count() // 每次渲染时候立即计算:计算桃子总价
// 点赞个数+1
const addLike = () => {
setLikeCount(likeCount + 1)
}
return (
<div>
<h3>店铺点赞数:{likeCount}</h3>
<button onClick={addLike}>点赞</button>
<h4> 购买的水果清单: </h4>
<h4> 桃子数量: {peach.num} 单价:{peach.unitPrice}元 </h4>
桃子总价:{price}元
<h4> 香蕉数量: {banana.num} 单价:{banana.unitPrice}元 </h4>
</div>
);
}
但是,如果我们只点击点赞事件,那么就会触发执行:setLikeCount(likeCount + 1),会走一次重新render。那么函数组件在每次重新渲染的时候就会再一次执行桃子总价js计算。显然,如果这部分计算非常耗时的话,那么每次点赞都会影响性能。显然,从也上看,只有桃子相关价格变化后我才需要执行这部分计算。 如下所示
let price = 0
function count() {
console.log("价格重新计算了--");
price = (peach.num * peach.unitPrice)
}
const countNum = useMemo(() => {
const result = count()
return result
}, [peach.num, peach.unitPrice]) // 每次渲染时候立即计算:计算桃子总价
return (
<div>
{{ countNum }} // 在useMemo中定义执行函数,该执行函数的返回值(有返回值)就是对应到视图的变量
</div>
)
因此,使用useMemo就登场了,和vue的computed类似,都是为了保证「部分js代码」不必在每次函数render时候都执行(进而优化组件更新时js的执行时间),而仅在依赖数据变化的时候才会执行。避免了在每个渲染阶段都执行高成本的计算。,重新渲染时候大量不必要且结果一致的重复js计算。
总结:
useMemo的优化目的:就是我们每次使用setState之后,一定会引起函数组件的重新渲染(异步),那么就会导致函数组件的重新执行函数体,那么有些函数的计算和本次更新的state无关,也被重复执行了。为了避免每次state更新后的组件渲染,导致函数不必要重复执行。因此将这部分(和state更新无关)函数执行返回值缓存,来缩短时间。
useMemo语法上:需要在第二个参数中注明依赖项,这样才能有效的监听到数据的变化,及时重新计算。如果监听数据没有变化,那么在组件重新渲染的时候,useMemo钩子对应的第一个函数参数不会再次执行,而是直接拿缓存数据。
2.2 useMemo和computed的区别
共同点:都是为了减少不必要的计算,优化组件的性能。
不同点:
| 区别 | computed | useMemo |
|---|---|---|
| 1.依赖项的收集监听 | 自动收集依赖项 | 手动添加依赖 |
| 2.执行时机 | 依赖变化时 | 组件重新渲染时执行 |
| 3.使用范围 | vue组件 | 函数式组件 |
1.依赖项的监听上:
computed是自动收集依赖项,因为在computed原理中,通过执行computed函数然后会触发响应式依赖数据的获取,这个时候会将computed函数放在依赖的订阅的队列中,这样当数据变化之后,遍历执行订阅队列,就可以自动执行computed函数。
useMemo是手动添加依赖,因为react本身没有双向绑定,因此视图的更新都是通过特定的dispatch函数执行的,因此就没有像vue那样的依赖收集阶段,故需要指定对应的依赖数据。
2.执行时机上 因为vue是在数据变化的时候,触发订阅队列,执行对应的update函数。但是react是在函数式组件重新渲染的时候,会生成新的FiberNode,然后对应的hooks链路会重新执行每一个hook,获取最新的数据,在这个时候useMemo才会去比较依赖数据的变化。
3.useCallback
3.1 使用场景
现状1:我们知道当我们使用setState去更新数据的时候,一定会引起函数式组件重新执行函数体,那么这个时候在函数组件内部中定义的函数就会重新创建,就会导致新创建的函数引用值和之前的引用值不一样。
现状2:我们知道当函数组件的重新渲染的条件是:state的变化 && props的变化 && 组件依赖的上下文变化 && 父组件的重新渲染(不论子组件的props和state是否变化)
那么我们通常基于现状2会有个避免子组件的重新渲染措施:
问题:父组件定义的函数作为参数传给子组件,然后父组件更新。但是一旦props发生变化,子组件还是会重新渲染。在此前提下,就会存在一个问题,因为react是“浅比较”,也就是复杂数据类型的时候,比较的是引用值。因此,如果父组件更新的话,那么重新执行函数体就会导致传给子组件的函数引用值变化,那么从表面上函数体不变,但是不函数的引用值变了,依然导致了子组件的重复渲染了。
目的:避免子组件在接受父组件函数体的时候,跟着父组件重新渲染 如下所示:
import React, { useCallback } from "react";
function ButtonComponent({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
function ParentComponent() {
const handleClick = useCallback(() => {
console.log("Button clicked"); }, []
);
return (
<div> <ButtonComponent onClick={handleClick}>Click me</ButtonComponent> </div> );
}
析:在这个例子中,我们创建了一个名为 ButtonComponent 的组件,它接受一个 onClick 函数属性。我们还创建了一个名为 ParentComponent 的组件,它使用 useCallback 钩子来创建一个 handleClick 函数。当 ParentComponent 重新渲染时,useCallback 会返回上一次创建的 handleClick 函数实例,避免了不必要的函数创建。
3.2 使用语法
接受两个参数:一个函数和一个依赖数组。
当依赖数组中的值发生变化时,useCallback 会返回一个新的函数实例。否则,它将返回上一次创建的函数实例。
const handleClick = useCallback(() => {
console.log("Button clicked"); }, []
);
然后useCallback返回的函数传递给子组件,作为props。