React.memo 与 useMemo
一、React.memo()
1、React.memo 的使用方式
React.memo() 文档地址:链接
在 class component 时代,为了性能优化我们经常使用 PureComponent,每次对props 进行一次浅比较。当然,除了 PureComponent 外, 我们还可以在 shouldComponentUpdate 中进行更深层次的控制。
在 Function Component 的使用中,React 贴心的提供了 React.memo 这个HOC (高阶组件),与 PureComponent 很相似,但是是专门给 Function Component 提供的,对 Class Component 并不适用。
但是相比于 PureComponent, React.memo() 相对于 PureComponent 来说, 用法更加方便。
当然,如果自己封装一个HOC,并且内部实现 PureComponent + shouldComponentUpdata 的结合使用,肯定也是OK的,在以往项目中,这样使用的方式还挺多
首先看下 React.memo() 的使用方式:
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
return true if passing nextProps to render would return
the same result as passing prevProps to render,
otherwise return false
*/
}
export default React.memo(MyComponent, areEqual);
使用方式很简单,在 Function Component 之外,再声明一个 areEqual 方法来判断两次 props 有什么不同,如果第二个参数不传递,则默认只会进行 props 的浅比较。
最终 export 的组件,就是React.memo() 包装之后的组件
2、React.memo 在性能优化的实践
理解上面三个组件代码即可理解 React.memo 的使用:
- index.js:父组件
- Child.js:子组件
- ChildMemo.js:使用 React.memo 包装过的子组件
1、index.js
父组件进行的逻辑很简单,就是引入两个子组件,并且将三个 state 通过 props 的方式传递给子组件
父组件本身进行的逻辑会进行三个 state 的变化
理论上,父组件每次变化一个 state 都通过 props 传递给了子组件,那子组件就会重新执行渲染。(无论子组件有没有真正用到这个props)
import React, { useState, } from 'react';
import Child from './Child';
import ChildMemo from './Child-memo';
export default (props = {}) => {
const [step, setStep] = useState(0);
const [count, setCount] = useState(0);
const [number, setNumber] = useState(0);
const handleSetStep = () => {
setStep(step + 1);
}
const handleSetCount = () => {
setCount(count + 1);
}
const handleCalNumber = () => {
setNumber(count + step);
}
return (
<div>
<button onClick={handleSetStep}>step is : {step} </button>
<button onClick={handleSetCount}>count is : {count} </button>
<button onClick={handleCalNumber}>numberis : {number} </button>
<hr />
<Child step={step} count={count} number={number} /> <hr />
<ChildMemo step={step} count={count} number={number} />
</div>
);
}
2、Child.js
这个子组件本身没有任何逻辑,也没有任何包装,就是渲染了父组件传递过来的 props.number。
需要注意的是,子组件中并没有使用到 props.step 和 props.count,但是一旦 props.step 发生了变化就会触发重新渲染
import React from 'react';
export default (props = {}) => {
console.log(`--- re-render ---`);
return (
<div>
{/* <p>step is : {props.step}</p> */}
{/* <p>count is : {props.count}</p> */}
<p>number is : {props.number}</p>
</div>
);
};
3、ChildMemo.js
这个子组件使用了 React.memo 进行了包装,并且通过 isEqual 方法判断只有当两次 props 的number 不等的时候才会重新出发渲染,否则console.log 也不会执行。
import React, { memo, } from 'react';
const isEqual = (prevProps, nextProps) => {
if (prevProps.number !== nextProps.number) {
return false;
}
return true;
}
export default memo((props = {}) => {
console.log(`--- memo re-render ---`);
return (
<div>
{/* <p>step is : {props.step}</p> */}
{/* <p>count is : {props.count}</p> */}
<p>number is : {props.number}</p>
</div>
);
}, isEqual);
4、效果对比:
通过上图可以发现,在点击 step 和 count 的时候,props.step 和 props.count 都发生了变化,因此 Child.js 这个子组件每次都在重新渲染 (----re-render----),即使没有用到这两个props。
而这种情况下, ChildMemo.js 则不会重新进行re-render。
只有当 props.number 发生变化的时候,ChildMemo.js 和 Child.js 表现是一致的。
2、使用 useMemo() 进行细粒度性能优化
上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写个方法比较那些具体的 props 不相同才进行 re-render。
而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现 局部 Pure 功能。
useMemo() 基本用法如下:
const memoizedvalue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo() 返回的是一个memoized 值, 只有当依赖项(比如上面的a,b 发生变化的时候,才会重新计算这个 memoized 值)
memoized 值不变的情况下,不会重新出发渲染逻辑。
说起渲染逻辑,需要记住的是 useMemo() 是在render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。
如果没有提供依赖数组(上面的 [a, b] ) 则每次都会重新计算 memoized 值,也就会 re-render
上面的代码中新增一个 Child-useMemo.js 子组件如下:
import React, { useMemo } from 'react';
export default (props = {}) => {
console.log(`--- component re-render ---`);
return useMemo(() => {
console.log(`--- useMemo re-render ---`);
return <div>
{/* <p>step is : {props.step}</p> */}
{/* <p>count is : {props.count}</p> */}
<p>number is : {props.number}</p>
</div>
}, [props.number]);
}
与上面唯一的区别是使用的 useMemo() 包装的是 return 部分渲染的逻辑,并且声明依赖了 props.number, 其他的并未发生变化。
效果对比:
上图我们可以发现,父组件每次更新 step/count 都会触发 useMemo 封装的子组件的 re-render,但是 number 没有变化,说明并没有重新出发HTML 部分吗 re-render
只有当依赖的props.number 发生变化的时候,才会重新出发 useMemo() 包装的里面的 re-render
总结:
React.memo() 跟 useMemo() 都可以实现对组件按需渲染的优化。
React.memo() 会在整个组件入口进行拦截,如果 isEqual 返回为 true, 则整个组件都不会执行
而useMemo() 可以更细致的操作 组件的 render 渲染,按照useMemo() 第二个参数数组 [] 依赖项来进行render,而不会拦截整个函数组件的其他业务逻辑