前言
在现代Web开发中,React已经成为构建用户界面的首选库之一。随着应用变得越来越复杂,性能优化成为确保良好用户体验的关键因素。
React组件默认会在状态或属性变化时重新渲染,即使某些子组件并不依赖于这些变化。这可能导致额外的计算开销,极大的消耗程序的运行速度,这显然是不可接受的,本文我们将探究这些新跟那个优化问题。
memo
memo
官称为React.memo
,官方介绍也就一句话:memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
ini
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
参数
Component
:要进行记忆化的组件。memo
不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和forwardRef
组件。- 可选参数
arePropsEqual
:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回true
。否则返回false
。通常情况下,你不需要指定此函数。默认情况下,React 将使用Object.is
比较每个 prop。
正常情况,我们只需要在父组件外部编写
const MemoizedChildComponent = memo(ChildComponent);
然后便可以在父组件内部使用MemoizedChildComponent
组件即可了,但是切忌这里是不能在父组件内部以表达式的形式使用,因为memo
的本质就是HOC组件(高阶组件),React的函数式编程是不允许在一个组件内部再次定义一个组件的。
这里我们要讲就讲点不一样改动,这里第二个参数的回调函数其实也是非常有趣的:arePropsEqual
typescript
import React, { useState, useEffect, memo } from 'react';
// 定义子组件的类型接口
interface PropsType {
counter: number;
}
// 子组件
function ChildComponent({ counter }: PropsType) {
console.log('子组件被渲染');
return (
<div>
{counter}
</div>
);
}
// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent);
// 父组件
export default () => {
const [counter, setCounter] = useState<number>(0);
// 处理计数器增加
const handleCounter = () => {
setCounter(prevState => prevState + 1);
};
console.log('父组件被渲染');
return (
<div>
<h1>父组件</h1>
<p>counter: {counter}</p>
<button onClick={handleCounter}>点击+1</button>
<MemoizedChildComponent counter={counter} />
</div>
);
};
上面我们正常使用是没问题的,子组件接受了父组件响应式,当该数据发生改变,子组件都会渲染。
但是当我们显示的指定第二个参数的返回值时,我们便可以用这个来控制子组件的响应式更新:
javascript
// 使用 memo 包裹子组件
const MemoizedChildComponent = memo(ChildComponent,(oldProps,newProps)=>true);
运行程序再来看看打印结果:
可以看到子组件在最开始便失去了父组件的响应式的更新,因此在业务开发中,某些子组件在做有限次任务时,内部便可指定第二个参数的判断,结合闭包使用便可达到效果。
useMemo
useMemo
用于缓存计算结果,有点Vue中的computed
的味道了。官方介绍:useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
ini
const cachedValue = useMemo(calculateValue, dependencies)
参数
calculateValue
:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果dependencies
没有发生变化,React 将直接返回相同值。否则,将会再次调用calculateValue
并返回最新结果,然后缓存该结果以便下次重复使用。dependencies
:所有在calculateValue
函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成[dep1, dep2, dep3]
这种形式。React 使用Object.is
将每个依赖项与其之前的值进行比较。
返回值
在初次渲染时,useMemo
返回不带参数调用 calculateValue
的结果。
在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue
,并返回最新结果。
这里我们便用一个示例搞清楚useMemo的使用:
javascript
import React, { useMemo, useState } from 'react';
export default function App() {
const [arr, setArr] = useState(new Array(10).fill(0));
const [expensiveValue, setExpensiveValue] = useState(null);
// 使用 useMemo 来记忆计算结果
const memoizedValue = useMemo(() => {
console.log('memoizedValue 被调起使用');
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
}, [arr.length]); // 仅当 arr.length 变化时重新计算
// 模拟一个昂贵的计算
const handleCalculate = () => {
setExpensiveValue(memoizedValue);
};
return (
<div>
<p>Array.length: {arr.length}</p>
<p>Value: {expensiveValue}</p>
<button onClick={() => setArr(prev => [...prev, 0])}>数组长度+1</button>
<button onClick={handleCalculate}>计算结果</button>
</div>
);
}
如果是没有useMemo的情况下,每次的组件更新这个memoizedValue
每次都将被调起执行,但是当我们使用useMemo之后,只有我们的依赖项改变之后,需要使用到它的计算结果之后才会取调起函数来使用。
useCallback
useCallback
用于函数。啥?缓存啥函数?,官文一句话:useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。
ini
const cachedFn = useCallback(fn, dependencies)
参数
fn
:想要缓存的函数。此函数可以接受任何参数并且返回任何值。在初次渲染时,React 将把函数返回给你(而不是调用它!)。当进行下一次渲染时,如果dependencies
相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。dependencies
:有关是否更新fn
的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像[dep1, dep2, dep3]
这样编写。React 使用Object.is
比较每一个依赖和它的之前的值。
返回值
在初次渲染时,useCallback
返回你已经传入的 fn
函数
在之后的渲染中, 如果依赖没有改变,useCallback
返回上一次渲染中缓存的 fn
函数;否则返回这一次渲染传入的 fn
。
useCallback
这个hook是非常有用的,一起先看一个小demo:
javascript
import React, { useState, useCallback,memo } from 'react';
const Child = ({ onClick }) => {
console.log('子组件被渲染');
return (
<button onClick={onClick}>
按钮
</button>
);
};
// 使用 memo 包裹子组件,以防止不必要的重新渲染
const ChildComponent=memo(Child);
export default ()=>{
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1);
console.log('按钮被点击,函数触发');
}
return (
<div>
<h1>父组件</h1>
<p>{count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
}
这个需求是十分正常的对吧,给子组件传入数据修改方法,但是每一次子组件还是会因为函数的传入而每次刷新,即使你使用memo来包裹。因为前面说了:memo
进行的是浅比较,即它只会检查对象或函数的引用是否相同。如果每次渲染时传递的新函数是一个新的函数实例,即使函数的内容没有改变,memo
也会认为 props 发生了变化。
这时我们就需要缓存function来减少渲染了,修改handleClick
函数:
ini
// 使用 useCallback 记忆 handleClick 函数
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
console.log('按钮被点击,函数触发');
}, []); // 空数组表示没有依赖项,函数永远不会改变
结语
memo,useMemo,useCallback便是我们前端react最主要的三大性能优化神器了。无论你是面试,还是真实的应用开发都是必须要熟练运用的,简单使用并不困难,进阶的话就需要深入研究了。