为什么会存在重复渲染?
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
以方法形式返回该对象,传入需要更新的依赖值