在对 React 项目做性能优化的时候,memeo、useMemo、useCallback 三个API总是形影不离。
一、memo
1.memo作用
在 React 的渲染流程中,一般来说,父组件的某个状态发生改变,那么父组件会重新渲染,父组件所使用的所有子组件,都会强制渲染。而在某些场景中,子组件并没有使用父组件传入的没有发生更改的状态时,子组件重新渲染是没有必要的。因此有了 React.memo
2.memo 的使用
memo 是个高阶组件, 结合了 PurComponent 和 shouldComponentUpdate 功能,会对传入的 props 进行浅比较,来决定是否更新被包裹的组件
memo 接受两个参数:
- WrapComponent:你要优化的组件
- (prev, next) => boolean:通过对比 prev(旧 props),next(新 props)是否一致,返回 true(不更新)、false(更新)
注意:memo 只针对 props 来决定是否渲染,且是浅比较
现在我们来看一个的例子:
import { useState } from 'react';
const Child = () => (
{console.log('子组件渲染了')}
);
function Parent() {
const [status, setStatus] = useState(true);
return (
);
}
export default Parent;
运行结果如下:
在上面的例子中,父组件中的状态 status和 Child 组件没有关系,当我点击按钮时,status 发生改变,此时父组件重新渲染,按钮文案变为off,控制台却打印出 "子组件又渲染" 的信息,说明子组件也跟着重新渲染了。而这肯定是不合理的,我们不希望子组件做无关的刷新,此时我们可以给子组件加上memo
import { useState, memo } from 'react';
const Child = memo(() => (
{console.log('子组件渲染了')}
));
function Parent() {
const [status, setStatus] = useState(true);
return (
);
}
export default Parent;
此时我们点击按钮,子组件不会被重新渲染
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
{props.number}
{console.log('子组件渲染了')}
));
Child.propTypes = {
number: PropTypes.number,
};
function Parent() {
const [number, setNumber] = useState(1);
return (
);
}
export default Parent;
在这个例子中,当我们点击按钮,传入子组件的number从1变为了2,子组件的props发生了改变,重新渲染
总而言之,如果组件被 memo 包裹,那么组件的 props 不发生改变时,组件不会重新渲染。这样,我们合理的使用 memo 就可以为我们的项目带来很大的性能优化
3.memo 的注意事项
memo 对于新旧 props 的比较默认是浅比较,当我们子组件接收的是一个引用类型的 props 的时候,可以自定义比较来决定是否需要使用缓存还是重新渲染
看下面的例子
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo((props) => (
{`我叫${props.obj.name}`}
{console.log('子组件渲染了')}
));
Child.propTypes = {
obj: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number,
}),
};
function Parent() {
const [obj, setObj] = useState({
name: 'xxx',
age: 18,
});
return (
);
}
export default Parent;
我们点击按钮修改了age,子组件的props发生了变化,重新渲染。但是子组件中并没有用到age,我们不需要它重新渲染,这个时候我们可以使用memo的第二个参数来自定义校验规则
import { useState, memo } from 'react';
import PropTypes from 'prop-types';
const Child = memo(
(props) => (
{`我叫${props.obj.name}`}
{console.log('子组件渲染了')}
),
// 新旧name相同就不重新渲染
(prev, next) => {
return prev.obj.name === next.obj.name;
},
);
Child.propTypes = {
obj: PropTypes.shape({
name: PropTypes.string,
age: PropTypes.number,
}),
};
function Parent() {
const [obj, setObj] = useState({
name: 'xxx',
age: 18,
});
return (
);
}
export default Parent;
这个时候我们点击按钮修改age,子组件就不会重新渲染了。注意:默认情况下(没有自定义校验)即使引用对象的属性值没发生变化,但是地址改变了,也会引起子组件重新渲染,例如上述例子中使用setObj({...obj})
因为缓存本身也是需要开销的。如果每一个组件都用 memo 去包裹一下,那么对浏览器的开销就会很大,本末倒置了。
所以我们应该选择性的用 memo 包裹组件,而不是滥用
二、useMemo
1.useMemo 的作用
useMemo 它可以缓存一个结果,当这个缓存结果不变时,可以借此来进行性能优化。
看下面的例子
import { useState } from 'react';
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = () => {
console.log('计算result');
for (let i = 0; i < 10000; i++) {
i.toString();
}
return 1000;
};
return (
result: {result()}
number: {number}
);
};
export default Parent;
当我们点击按钮,number每次点击都会加1,result方法也会随着重新计算一遍,每次都要进行大量的for循环,很耗费性能,这种情况下我们可以使用useMemo来进行优化
2.useMemo 的使用
useMemo 接受两个参数:
- callback:计算结果的执行函数
- deps:相关依赖项数组
最终 useMemo 在执行了 callback 后,返回一个结果,这个结果就会被缓存起来。当 deps 依赖发生改变的时候,会重新执行 callback 计算并返回最新的结果,否则就使用缓存的结果
我们来把上面的例子用 useMemo 改造一下
import { useState, useMemo } from 'react';
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useMemo(() => {
console.log('计算result');
for (let i = 0; i < 10000; i++) {
i.toString();
}
return 1000;
}, []);
return (
result: {result}
number: {number}
);
};
export default Parent;
现在不论我们怎么去改变number的值,result都不会重新运行,这样就达到了性能优化的目的
useMemo 并不是用的越多越好,缓存本身也需要开销,一些简单的计算方法就没必要使用useMemo
3.useMemo配合memo使用
import { useState, memo } from 'react';
const Child = memo(() => {
console.log('子组件渲染');
return 子组件;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = () => {
console.log('计算result');
return 1000;
};
return (
result: {result}
number: {number}
);
};
export default Parent;
上面的例子中,result函数作为props传给了子组件,即使子组件被memo包裹着,但还是重新渲染了,这是因为,父组件重新渲染时,又创建了一个函数(或者说又开辟了一个内存地址)赋值给 result,而 memo 只做浅比较,发现地址改变了,所以子组件重新渲染,这个时候就需要使用 useMemo 来进行优化
import { useState, memo, useMemo } from 'react';
const Child = memo(() => {
console.log('子组件渲染');
return 子组件;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useMemo(() => {
console.log('计算result');
return 1000;
}, []);
return (
result: {result}
number: {number}
);
};
export default Parent;
此时,再次点击按钮修改 number 后,子组件不会重新更新,达到了性能优化的目的
三、useCallback
1.useCallback 的作用
useCallback 类似于 useMemo,只不过 useCallback 用于缓存函数罢了,同样可以防止无关的刷新,对组件做出性能优化
2.useCallback 的使用
useCallback 同样接受两个参数:
- callback:传入子组件的函数
- deps:相关依赖项数组
最终 useCallback 会把传入的 callback 缓存起来。当 deps 依赖发生改变的时候,会重新缓存最新的 callback ,否则就使用缓存的结果
单独使用 useCallback 起不到优化的作用,反而会增加性能消耗,需要和 memo 一起使用
我们来把上面的例子用 useCallback 改造一下
import {
useState,
memo,
useCallback,
} from 'react';
const Child = memo(() => {
console.log('子组件渲染');
return 子组件;
});
const Parent = () => {
const [number, setNumber] = useState(0);
function addNumber() {
setNumber(number + 1);
}
const result = useCallback(() => {
console.log('计算result');
}, []);
return (
result: {result}
number: {number}
);
};
export default Parent;
点击按钮修改 number 后,子组件不会重新更新,达到了性能优化的目的
总结
memo:
- 父组件重新渲染,没有被 memo 包裹的子组件也会重新渲染
- 被 memo 包裹的组件只有在 props 改变后,才会重新渲染
- memo 只会对新旧 props 做浅比较,所以对于引用类型的数据如果发生了更改,需要返回一个新的地址
- memo 并不是用的越多越好,因为缓存本身也是需要开销的。如果每一个组件都用 memo 去包裹一下,那么对浏览器的开销就会很大,本末倒置了
- 项目中可以针对刷新频率高的组件,根据实际情况,使用 memo 进行优化
useMemo:
- useMemo 是对计算的结果进行缓存,当缓存结果不变时,会使用缓存结果
- useMemo 并不是用的越多越好,对于耗时长、性能开销大的地方,可以使用 useMemo 来优化,但大多数情况下,计算结果的开销还没有使用 useMemo 的开销大,应视情况而定
- 当父组件传了一个引用类型的结果 result 给子组件,且子组件用 memo 包裹时,需要使用 useMemo 对 result 进行缓存,因为 memo 只对 props 做浅比较,当父组件重新渲染时,会重新在内存中开辟一个地址赋值给 result,此时地址发生改变,子组件会重新渲染
useCallback:
- useCallback 与 useMemo 类似,只不过是对函数进行缓存
- useCallback 可以单独使用,但是单独使用的使用对性能优化并没有实质的提升,且父组件此时重新渲染,子组件同样会渲染
- useCallback 需要配合 memo 一起使用,这样当父组件重新渲染时,缓存的函数的地址不会发生改变,memo 浅比较会认为 props 没有改变,因此子组件不会重新渲染