React.memo
什么是memo
React.memo是一个官方内置的高阶组件,使用memo包裹你的组件,当父组件重新渲染时,如果props没有发生改变,那子组件将不会重新渲染。
React一次更新都会重新构建一颗组件树,经常会有很多不必要的开销,React.memo是一种性能优化的手段,让你可以跳过开销较大的组件的render。
官方文档:react.docschina.org/reference/r…
memo如何对比props
默认情况下,会使用Object.is比较每一个prop;memo的第二个参数接受一个对比函数,可以手动比较决定是否跳过更新。
memo(Component, function (oldProps, newProps) {
return oldProps.id === newProps.id;
})
何时使用memo
官方推荐React.memo只在必要的时候使用,滥用会导致代码复杂度上升且增加阅读成本。
必要的场景同时满足:
1.相同的props总是渲染出想同的UI结果。
2.组件重新渲染出现严重延迟,不优化无法正常运行。
具体用例
首先我们写一个耗时组件, 经测试这个组件的render耗时可能长达1s以上(根据运行环境有差异)
export default function SlowComponent(props) {
console.log('SlowComponent render start');
const start = Number(new Date());
let i = 0;
while (i < 1000000000) {
i += 1;
}
const end = Number(new Date());
console.log('SlowComponent render end', end - start);
return <div>{props.value}</div>
}
我们在页面中引入这个组件,以下是个很简单的页面,由一个计数元素、一个按钮和一个耗时组件组成。
function App() {
const [count, setCount] = useState(0);
const [value , setValue] = useState('');
return (
<div className="App">
<div className="App-body">
<div>count: { count }</div>
<div onClick={() => setCount(count => count + 1)}>click</div>
<SlowComponent value={value}/>
</div>
</div>
);
}
// 点击按钮,打印SlowComponent render start
当我们点击按钮时,组件App的内部状态改变并重新渲染,这会导致所有自组件都触发重新渲染,包括耗时组件(尽管每个prop值都是相同的)。
此时,未优化耗时组件将渲染1s以上,新组件树的构建耗时被严重拖慢,导致视觉上的更新卡顿。
我们使用memo包裹耗时组件进行优化
export default React.memo(SlowComponent);
// 点击按钮,不打印SlowComponent render start
此时点击按钮,控制台将不在打印“SlowComponent render start”,耗时组件的渲染被跳过了,整个组件树的构建时间减去了一秒或更多,更新变的很流畅。
prop为对象、数组或者函数
上文提到默认的比较函数是使用Object.is对每个prop进行比较(浅对比),那么就会出现一个新的问题:当对象、数组、函数的引用改变时,“记忆化”就失效了。
以函数为例,我们修改上一小节的代码:让用户点击SlowComponent对应的元素后计数+1:
//App.js
function App() {
const [count, setCount] = useState(0);
const [value , setValue] = useState('点击 + 1');
const [state] = useReducer(reducer, initState);
const handleClick = () => setCount(count => count + 1);
return (
<div className="App">
<header className="App-header">
<div id="title">{state.title}</div>
</header>
<div className="App-body">
<div>count: { count }</div>
<SlowComponent value={value} handleClick={handleClick}/>
</div>
</div>
);
}
//SlowComponent.js
function SlowComponent(props) {
console.log('SlowComponent render start');
const start = Number(new Date());
let i = 0;
while (i < 1000000000) {
i += 1;
}
const end = Number(new Date());
console.log('SlowComponent render end', end - start);
return <div onClick={props.handleClick}>{props.value}</div>
}
export default React.memo(SlowComponent)
修改后的ui如下图:
点击“点击 + 1”后触发更新,此时控制台打印了SlowComponent render start,页面卡顿后更新
这是因为App每次render都创建了一个新的handleClick函数,引用和之前不一样了。要解决这个问题,需要让我们的handleClick也实现“记忆化”,这就需要用到useCallback或者useMemo。
useMemo & useCallback
我们尝试使用useCallback来优化上文的代码:
function App() {
const [count, setCount] = useState(0);
const [value , setValue] = useState('点击 + 1');
const [state] = useReducer(reducer, initState);
// 修改这一行
const handleClick = useCallback(() => setCount(count => count + 1), []);
return (
<div className="App">
<header className="App-header">
<div id="title">{state.title}</div>
</header>
<div className="App-body">
<div>count: { count }</div>
<SlowComponent value={value} handleClick={handleClick}/>
</div>
</div>
);
}
我们再次点击,发现页面不再卡顿更新,控制台未打印SlowComponent render start,SlowComponent没有re-render。
使用useMemo也可以实现类似的效果:
const handleClick = useMemo(() => () => setCount(count => count + 1), []);
对于prop是对象、数组、函数这种会改变引用的情况,需要借助useMemo和useCallback,让对应的prop具有“记忆化”功能,否则我们的性能优化很容易就失效