在hooks诞生以前,若是组件包含内部 state
,咱们都是基于 class
的形式来建立组件。css
在react中,性能优化的点在于:react
setState
,就会触发组件的从新渲染,不管先后 state
是否相同基于上面的两点,咱们一般的解决方案是:使用 immutable
进行比较,在不相等的时候调用 setState
, 在 shouldComponentUpdate
中判断先后的 props
和 state
,若是没有变化,则返回 false
来阻止更新。ios
在 hooks
出来以后,函数组件中没有 shouldComponentUpdate
生命周期,咱们没法经过判断先后状态来决定是否更新。useEffect
再也不区分 mount
update
两个状态,这意味着函数组件的每一次调用都会执行其内部的全部逻辑,那么会带来较大的性能损耗。c++
咱们先简单的看一下useMemo和useCallback的调用签名:typescript
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不能。api
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
来访问。