为什么会存在重复渲染?
react 在 v16.8 版本引入了全新的 api,叫做 React Hooks,它的使用与以往基于 class component 的组件用法非常的不一样,不再是基于类,而是基于函数进行页面的渲染,我们把它又称为 functional component。
因为 react hook 使用的是函数组件,父组件的任何一次修改,都会导致子组件的函数执行,从而重新进行渲染。
那么下面我们考虑三种情况:
- 父组件没有 props 传入子组件 props
- 父组件传入子组件的 props 都是简单数据类型
- 父组件传入子组件的 props 存在复杂数据类型
React.memo 为高阶组件。它与 React.PureComponent 非常相似。默认只会对复杂类型对象做浅层比较,如果需要控制对比过程我们可以将比较函数作为第二个参数传入:React.memo(MyComp, areEqual)
父组件没有 props 传入子组件 props
在这种情况下,子组件的渲染不需要依赖父组件值的变化,使用 React.memo 包裹子组件,即缓存下子组件。这样,父组件中的数值如何变化,都会使用缓存下来的子组件。
父组件传入子组件的 props 都是简单数据类型
在这种情况下,父组件传入子组件的 props 都是简单数据类型,浅层对比即可判断是否发生了变化,使用 React.memo 包裹子组件,也可以解决重复渲染的问题。
父组件传入子组件的 props 存在复杂数据类型
父组件通过 props 向子组件传值时,可能需要传入复杂类型如 object,以及 function 类型的值。而 memo 子组件进行渲染比对时进行的是浅比较,即使我们传入相同的 object 或 function,子组件也会认为传入参数存在修改,从而子组件重新进行渲染。这个时候仅仅使用 memo 包裹子组件应该没办法解决问题了,是时候用上我们的 useCallback 以及 useMemo 了。
下面我们来看一下 React.memo 的使用。
React.memo 的使用
例如,一个父组件 Home 中渲染了子组件 List,同时 Home 组件还有一个计数器组件,每次点击 count 都会加 1,遇到类似的场景就会出现子组件重复渲染问题,这是因为 React 中当父组件的一个状态改变后,无论和子组件是否有关,子组件都会受到影响进行重新渲染,这也是 React 中默认的一个行为。
函数组件中的解决方案是使用 React.memo() 函数,将需要优化的函数组件传入即可。
import React, { useEffect, useState } from "react";
// 未使用 memo:const List = ({ dataList }) => {
const List = React.memo(({ dataList }) => {
console.log("List 渲染");
return (
<div>
{dataList.map((item) => (
<h2 key={item.id}> {item.title} </h2>
))}
</div>
);
});
const Home = () => {
const [count, setCount] = useState(0);
const [dataList, setDataList] = useState([]);
useEffect(() => {
const list = [
{ title: "React 性能优化", id: 1 },
{ title: "Node.js 性能优化", id: 2 },
];
setDataList(list);
}, []);
return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
count: {count}
</button>
<List dataList={dataList} />
</div>
);
};
export default Home;
自定义控制对比过程
函数 React.memo() 还提供了第二个参数 propsAreEqual,用来自定义控制对比过程。
// React.memo() 的 TypeScript 类型描述
function memo<T extends ComponentType<any>>(
Component: T,
propsAreEqual?: (
prevProps: Readonly<ComponentProps<T>>,
nextProps: Readonly<ComponentProps<T>>
) => boolean
): MemoExoticComponent<T>;
使用memo, useMemo, useCallback
useCallback
先来说下,经常使用一些的 useCallback
// 仅使用了memo,父组件传递给子组件的prop为方法,
// 该方法在子组件中被调用,改变了父组件的值,导致父组件重新渲染。
// 又由于父组件重新渲染,传给子组件的方法因其引用地址的不同会被认为有修改,导致子组件出现了不必要的重新渲染。
const Child = memo((props) => {
console.log('我是一个子组件');
return (
<button onClick={props.handleClick}>改变父组件中的年龄</button>
)
})
const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
return (
<div>
<span>`目前的count值为${age}`<span>
<Child handleClick={() => setAge(age + 1)}/>
</div>
)
}
// 使用了useCallback优化了传递给子组件的函数,只初始化一次这个函数,下次不产生新的函数
const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child handleClick={useCallback(() => setAge(age + 1), [])}/>
</div>
)
}
注意:在 useCallback 的第二个参数处要传入正确的依赖值,否则 useCallback 就不会重新执行,其中使用的变量就还是之前的值,useMemo也是如此。
我们在方法中可能会使用一些组件中但是存在方法外的参数,我们一定要将这些参数放入依赖项中,否则会一直使用缓存的方法,里面的外部参数也会一直是旧值。
useMemo
// 使用了memo以及useCallback,我们会发现更新属性profile为对象时,
// 尽管子组件只改变了age的值且子组件并没有使用age字段,子组件还是执行了。
// 这是因为在父组件更新其他状态的情况下,子组件的profile作为复杂类型,
// 仅仅进行浅比较会被认为存在修改,从而会一直重新渲染改变,导致子组件函数一直执行,这也是不必要的性能浪费。
// 解决这个问题,就需要在profile属性上使用useMemo了
const Child = memo((props) => {
console.log('我是一个子组件');
const {profile, handleClick} = props;
return (
<div>
<div>{`父组件传来的用户信息:姓名${profile.name}, 性别${profile.gender}`}</div>
<button onClick={handleClick}>改变父组件age</button>
</div>
)
})
const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
const [name, setName] = useState('张三男');
const
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child
profile={{name, gender: name.indexOf('男') > -1 ? '男' : '女' }}
handleClick={useCallback(() => setAge(age + 1), [])}
/>
</div>
)
}
// 使用useMemo,返回一个和原对象一样的对象,第二个参数是依赖性,仅当name发生改变的时候,才产生一个新的对象,注意:依赖项千万要填写正确,否则name改变时,profile依旧使用旧值,就会产生错误
const Father = () => {
console.log('我是一个父组件')
const [age, setAge] = useState(0);
const [name, setName] = useState('张三男');
const
return (
<div>
<span>`目前的年龄为${age}`<span>
<Child
profile={useMemo(() => ({
name,
gender: name.indexOf('男') > -1 ? '男' : '女' }), [name])
}
handleClick={useCallback(() => setAge(age + 1), [])}
/>
</div>
)
}
React.memo 无效情况
第一种
React.memo 对普通的引用类型是无效的。例如,在 List 组件增加 user 属性,即使使用了 React.memo() ,每次点击 count, List 组件还会重复渲染。
const Home = () => {
const user = {name: '哈哈'};
...
return (
<div>
<List dataList={dataList} user={user} />
</div>
);
};与 React.memo() 结合使用时,普通引用类型对象需要通过 useMemo、useState 处理,来避免组件的重复渲染。
const user = useMemo(() => ({ name: "哈哈" }), []);
const [user] = useState({ name: "哈哈" });第二种
函数组中包括了一些 Hooks 例如 useState、useContext,当上下文发生变化时,组件也同样会重新渲染,React.memo 在这里仅比较 props。上面例子中,如果把 button 组件放到 List 组件里,每次点击,List 也还是会被重新渲染。
const List = React.memo(({ dataList }) => {
console.log("List 渲染");
const [count, setCount] = useState(0);
return (
<div>
<button type="button" onClick={() => setCount(count + 1)}>
List count: {count}
</button>
{dataList.map((item) => (
<h2 key={item.id}> {item.title} </h2>
))}
</div>
);
});总结
React.memo() 是一个高阶组件,接收一个组件并返回一个新组件。它会记忆组件上次的 Props,同下次需要更新的 Props 做 “浅对比”,如果相同就不做更新,只有在不同时才会重新渲染。如果你的组件存在一些耗时的计算,每次重新渲染对页面性能显然是糟糕的,这时 React.memo() 对你来说也许是一个好的选择。并不是所有的组件都要引入 React.memo(),自身浅对比这个过程也会有一些消耗,如果没有特殊需求,也不一定非要使用。
- 子组件没有从父组件传入的 props 或者传入的 props 仅仅为简单数值类型使用
memo 即可 - 子组件有从父组件传来的方法时,在使用
memo 的同时,使用 useCallback 包裹该方法,传入方法需要更新的依赖值 - 子组件有从父组件传来的对象和数组等值时,在使用
memo 的同时,使用 useMemo 以方法形式返回该对象,传入需要更新的依赖值










