在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
返回缓存的 函数
在 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 包装过的子组件
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> ); } 复制代码
这个子组件本身没有任何逻辑,也没有任何包装,就是渲染了父组件传递过来的 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> ); }; 复制代码
这个子组件使用了 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.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
讲完了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
来访问。