前言
一、 类组件过渡到函数组件
- 类组件以前在组件之间复用状态逻辑很难
react 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。有一些解决此类问题的方案 ,比如render props 和 高阶组件。但会重新组织你的组件结构,会很麻烦,使代码难以理解。而且render props 等其他抽象层组成的组件会形成“嵌套地狱”。所以React 需要为共享状态逻辑提供更好的原生途径。
- 复杂组件变得难以理解
组件常常在
componentDidMount和componentDidUpdate中获取数据。但是,componentDidMount中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
3.难以理解的class
除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中
this的工作方式
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决案。
二、 Hooks
- React Hooks 出来很长一段时间了,鉴于使用 react 的结果,给大家分享一些 React Hooks 性能优化。
React 性能优化思路
一、 我觉得 React 性能优化的理念的主要方向就是这两个:
- 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。
- 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。
在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdate和 PureComponent,这两个 API 所提供的解决思路都是为了减少重新 render 的次数,主要是减少父组件更新而子组件也更新的情况.
但是在函数式组件里面没有声明周期也没有类,那如何来做性能优化呢?
举个例子
现在有个父子组件,子组件依赖父组件传入的 name 属性,但是父组件 name 属性和 text 属性变化都会导致 Parent 函数重新执行,所以即使传入子组件 props 没有任何变化,甚至子组件没有依赖于任何 props 属性,都会导致子组件重新渲染
const Child = ((props: any) => {
console.log("我更新了...");
return (
<div>
<h3>子组件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
上面的代码执行你会发现,不管是触发 handleInputChange 还是触发 handleClick ,子组件都会在控制台输出 ,"我更新了... " 所以即使传入子组件 props 没有任何变化,甚至子组件没有依赖于任何 props 属性,子组件都会重新渲染
结果如下图:
想要解决重复渲染的问题,可以使用react的亲手制造升级的儿子,他有三个方法用来做优化,分别是 React.memo useCallback useMemo 。
React.memo
使用 memo 包裹子组件时,只有 props 发生改变子组件才会重新渲染。使用 memo 可以提升一定的性能。React.memo在给定相同props的情况下渲染相同的结果,并且通过记忆组件渲染结果的方式来提高组件的性能表现。
const Child = React.memo((props: any) => {
console.log("我更新了..."); // 只有当props属性改变,即name属性改变时,子组件才会重新渲染
return (
<div>
<h3>子组件</h3>
<div>text:{props.name}</div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
const handleInputChange = (e) => {
setText(e.target.value)
}
return (<div>
<input onChange={handleInputChange} />
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name ={text}/>
</div>)
}
此时结果如下:
React.memo 高级用法
默认情况下其只会对 props 的复杂对象做浅层对比(浅层对比就是只会对比前后两次 props 对象引用是否相同,不会对比对象里面的内容是否相同),如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function Child(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 falseChild
*/
if (prevProps.name === nextProps.name) {
return true; // 即子组件不会重新render
} else {
return false;
}
}
export default React.memo(Child, areEqual);
React.memo 原理
export default function memo<Props>(
type: React$ElementType,
compare?: (oldProps: Props, newProps: Props) => boolean,
) {
if (__DEV__) {
// do something
}
return {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
}
memo 方法接受两个参数:
type:必选参数,它是一个 React 组件。compare:可选参数,它是一个返回布尔值的比较函数。用于比较前后两个 props 是否相等。
useCallback
但如果传入的 props 包含函数,父组件每次重新渲染都是创建新的函数,所以传递函数子组件还是会重新渲染,即使函数的内容还是一样。如何解决这一问题,我们希望把函数也缓存起来,于是引入 useCallback
useCallback 用于缓存函数,只有当依赖项改变时,函数才会重新执行返回新的函数,对于父组件中的函数作为 props 传递给子组件时,只要父组件数据改变,函数重新执行,作为 props 的函数也会产生新的实例,导致子组件的刷新, 使用 useCallback 可以缓存函数。需要搭配 memo 使用
把函数以及依赖项作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。
const Child = React.memo((props: any) => {
console.log("我更新了...");
return (
<div>
<h3>子组件</h3>
<div>text:{props.name}</div>
<div> <input onChange={props.handleInputChange} /></div>
<div>{new Date().getTime()}</div>
</div>
)
})
const Parent = () => {
const [count, setCount] = useState(0);
const [text, setText] = useState("")
const handleClick = () => {
setCount(count + 1);
}
// 通过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
const handleInputChange = useCallback((e) => {
setText(e.target.value )
},[])
return (<div>
<button onClick={handleClick}>+1</button>
<div>count:{count}</div>
<Child name={text} handleInputChange={handleInputChange}/>
</div>)
}
结果如下:
useCallback 第二个参数依赖项什么情况下使用呢,看下面的例子
//修改handleInputChange
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[])
如下图:
count 改变,但 handleInputChange 不依赖与任何项,所以 handleInputChange 只在初始化的时候调用一次函数就被缓存起来,当文本改变时或者 count 改变时函数内部的 count 始终为 0,所以需要将 count 加入到依赖项,count 变化后重新生成新的函数,改变函数内部的 count 值
const handleInputChange =useCallback((e) => {
setText(e.target.value + count)
},[count])
useCallback原理
首次挂载组件时,走的时mount**, 组件更新时走的是update**
// mount阶段就是获取到传入的回调函数和依赖数组,保存到hook的memorizedState中,然后返回回调函数。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
// update阶段
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
// 从hook的memorizedState中获取上次保存的值[callback, deps],
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 比较新的deps和之前的deps是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相等,返回memorized的callback
return prevState[0];
}
}
}
// 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
hook.memoizedState = [callback, nextDeps];
return callback;
}
useMemo
React 的性能优化方向主要是两个:一个是减少重新 render 的次数(或者说减少不必要的渲染),另一个是减少计算的量。
前面介绍的 React.memo 和 useCallback 都是为了减少重新 render 的次数。对于如何减少计算的量,就是 useMemo 来做的
对于如何减少计算的量,就是 useMemo 来做的,useMemo 使用场景请看下面这个例子
const Parent =()=> {
const [num, setNum] = useState(0);
// 一个非常耗时的一个计算函数
// result 最后返回的值是 49995000
const expensiveFn=()=> {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result) // 49995000
return result;
}
const base = expensiveFn();
return (
<div className="Parent">
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
如下图:
useMemo 做计算结果缓存
针对上面产生的问题,就可以用 useMemo 来缓存 expensiveFn 函数执行后的值。
const computeExpensiveValue=()=> {
// 计算量很大的代码
return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
useMemo 的第一个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会作为 useMemo 的返回值,第二个参数是一个数组依赖,如果数组里面的值有变化,那么就会重新去执行第一个参数里面的函数,并将函数返回的值缓存起来并作为 useMemo 的返回值 。
了解了 useMemo 的使用方法,然后就可以对上面的例子进行优化,优化代码如下:
const Parent =()=> {
const [num, setNum] = useState(0);
const expensiveFn=()=> {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result)
return result;
}
const base = useMemo(expensiveFn, []);
return (
<div className="Parent">
<h1>count:{num}</h1>
<button onClick={() => setNum(num + base)}>+1</button>
</div>
);
}
执行上面的代码,然后现在可以观察无论我们点击 +1多少次,只会输出一次 49995000,这就代表 expensiveFn 只执行了一次,达到了我们想要的效果。
useMemo原理
useMemo和useCallback的源码部分比较相似.
// mount阶段, 执行创建函数获得返回值
// 保存到hook的memorizedState中[nextValue, nextDeps]
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// update阶段
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
// 获取新的deps
const nextDeps = deps === undefined ? null : deps;
// 从memorizedState中获得上次保存的值
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
// 比较新deps和旧deps是否相等,如果两者相等,返回旧的创建函数的返回值
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 如果deps发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
总结
对于性能优化有很多方面:网络、关键路径渲染、打包、图片、缓存等等方面,我只介绍了性能优化中的冰山一角:运行过程中 React 的优化。
React 的优化方向:减少 render 的次数;减少重复计算。
合理的拆分组件其实也是可以做性能优化的,你这么想,如果你整个页面只有一个大的组件,那么当 props 或者 state 变更之后,需要 reconction 的是整个组件,其实你只是变了一个文字,如果你进行了合理的组件拆分,你就可以控制更小粒度的更新。