前言
在hooks诞生之前,如果组件包含内部 state
,我们都是基于 class
的形式来创建组件。
在react中,性能优化的点在于:
- 调用
setState
,就会触发组件的重新渲染,无论前后state
是否相同 - 父组件更新,子组件也会自动更新
基于上面的两点,我们通常的解决方案是:使用 immutable
进行比较,在不相等的时候调用 setState
, 在 shouldComponentUpdate
中判断前后的 props
和 state
,如果没有变化,则返回 false
来阻止更新。
在 hooks
出来之后,函数组件中没有 shouldComponentUpdate
生命周期,我们无法通过判断前后状态来决定是否更新。useEffect
不再区分 mount
update
两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。
对比
我们先简单的看一下useMemo和useCallback的调用签名:
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T; function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
useCallback
和 useMemo
的参数跟 useEffect
一致,他们之间最大的区别有是 useEffect
会用于处理副作用,而前两个hooks不能。
useCallback
和 useMemo
都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo
返回缓存的 变量,useCallback
返回缓存的 函数
React.memo()
在 class 组件时代,为了性能优化我们经常会选择使用 PureComponent
,每次对 props 进行一次浅比较,当然,除了 PureComponent 外,我们还可以在 shouldComponentUpdate
中进行更深层次的控制。
在 Function 组件中, React 贴心的提供了 React.memo
这个 HOC(高阶组件),与 PureComponent 很相似,但是是专门给 Function Component 提供的,对 Class Component 并不适用。
但是相比于 PureComponent ,React.memo()
可以支持指定一个参数
,可以相当于 shouldComponentUpdate
的作用,因此 React.memo() 相对于 PureComponent 来说,用法更加方便。
(当然,如果自己封装一个 HOC,并且内部实现 PureComponent + shouldComponentUpdate 的结合使用,肯定也是 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() 包装之后的组件。
实例:
-
index.js:父组件
-
Child.js:子组件
-
ChildMemo.js:使用 React.memo 包装过的子组件
index.js
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>
);
}
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>
);
};
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);
效果对比
通过上图可以发现,在点击 step 和 count 的时候,props.step 和 props.count 都发生了变化,因此 Child.js
这个子组件每次都在重新执行渲染(----re-render----
),即使没有用到这两个 props。
而这种情况下,ChildMemo.js
则不会重新进行 re-render。
只有当 props.number 发生变化的时候,ChildMemo.js
和 Child.js
表现是一致的。
从上面可以看出,React.memo() 的第二个方法在某种特定需求下,是必须存在的。 因为在实验的场景中,我们能够看得出来,即使我使用 React.memo
包装了 Child.js,也会一直触发重新渲染,因为 props 浅比较肯定是发生了变化。
React.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-redner
上面的代码中新增一个 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.useCallback()
讲完了useMemo,接下来是 useCallback
。useCallback 跟 useMemo 比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:
const fnA = useCallback(fnB, [a])
实例:
import React, { useState, useCallback } from 'react';
import Button from './Button';
export default function App() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const handleClickButton1 = () => {
setCount1(count1 + 1);
};
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
return (
<div>
<div>
<Button onClickButton={handleClickButton1}>Button1</Button>
</div>
<div>
<Button onClickButton={handleClickButton2}>Button2</Button>
</div>
</div>
);
}
Button组件
// Button.jsx
import React from 'react';
const Button = ({ onClickButton, children }) => {
return (
<>
<button onClick={onClickButton}>{children}</button>
<span>{Math.random()}</span>
</>
);
};
export default React.memo(Button);
这里或许会注意到
React.memo
这个方法,此方法内会对props
做一个浅层比较,如果如果props
没有发生改变,则不会重新渲染此组件。
上面的 Button
组件都需要一个 onClickButton 的 props ,尽管组件内部有用 React.memo
来做优化,但是我们声明的 handleClickButton1
是直接定义了一个方法,这也就导致只要是父组件重新渲染(状态或者props更新)就会导致这里声明出一个新的方法,新的方法和旧的方法尽管长的一样,但是依旧是两个不同的对象,React.memo
对比后发现对象 props 改变,就重新渲染了。
const handleClickButton2 = useCallback(() => {
setCount2(count2 + 1);
}, [count2]);
上述代码我们的方法使用 useCallback 包装了一层,并且后面还传入了一个 [count2]
变量,这里 useCallback 就会根据 count2 是否发生变化,从而决定是否返回一个新的函数,函数内部作用域也随之更新。
由于我们的这个方法只依赖了 count2 这个变量,而且 count2 只在点击 Button2 后才会更新 handleClickButton2
,所以就导致了我们点击 Button1 不重新渲染 Button2 的内容。
总结
-
在子组件不需要父组件的值和函数的情况下,只需要使用
memo
函数包裹子组件即可。 -
如果有函数传递给子组件,使用
useCallback
-
如果有值传递给子组件,使用
useMemo
-
useEffect
、useMemo
、useCallback
都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state
,props
),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref
来访问。