React 核心开发团队一直都致力于提高 React 渲染速度。 React 16 就引入了 React.memo
(16.6.0),React.useCallback
与 React.useMemo
(React Hooks 特性 16.8.0)都是用于优化 React 组件性能。
React.memo
React.memo
一个用于避免组件无效重复渲染的高价组件。与 React.PureComponent
组件和 shouldComponentUpdate()
方法功能类似。但 React.memo
只能用于函数组件,并且如果 React.memo
接受第二个参数 compare
,compare
返回值为 true
不渲染,false
则渲染。这与 React.PureComponent
组件和 shouldComponentUpdate()
方法刚好相反。在线实例:
import * as React from 'react';
import {
Button, Typography } from 'antd';
const ChildComponent = () => {
console.log('子组件 ChildComponent');
return (
);
};
const ParentComponent = () => {
const [count, setCount] = React.useState < number > 0;
return (
count:{
count}
);
};
export default ParentComponent;
运行,每次单击【+1 按钮】,都会导致 ChildComponent
组件重新渲染:
React 渲染机制,ParentComponent
的更新会重新渲染 ChildComponent
组件。如果想用 ChildComponent
组件不渲染,这里可以使用 React.memo
高级组件来优化。在线实例
const ChildComponent = React.memo(() => {
console.log('子组件 ChildComponent');
return (
);
});
使用 React.memo
对 ChildComponent
进行包裹:
从实例可以清楚地知道,ChildComponent
组件被 React.memo
包装后,父组件 ParentComponent
的更新不会引起 ChildComponent
组件重新渲染。
singsong: 如果想更精确控制React.memo
包裹组件何时更新,可以传入React.memo
的第二个参数compare
。因为React.memo
默认只会对props
做 浅层对比 shallowEqual。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function compare(prevProps, nextProps) {
/* 比较 prevProps 与 nextProps */
// 如果为 true 表示该组件不需要重新渲染,如果为 false 表示重新渲染该组件
}
export default React.memo(MyComponent, compare);
singsong: 如果React.memo
包裹的函数组件中使用了useState
、useContext
,当 context 与 state 变化时,也会导致该函数组件的更新。
React.memo 关键源码
import {
REACT_MEMO_TYPE } from 'shared/ReactSymbols';
export function memo(type: React$ElementType, compare?: (oldProps: Props, newProps: Props) => boolean) {
const elementType = {
$$typeof: REACT_MEMO_TYPE,
type,
compare: compare === undefined ? null : compare,
};
return elementType;
}
React.memo
props
渲染输出不变)props
基本不变React.useCallback
在介绍 useCallback
之前,先来看看如下实例的输出:
function sumFactory() {
return (a, b) => a + b;
}
const sum1 = sumFactory();
const sum2 = sumFactory();
console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true
sumFactory
工厂方法返回的两个方法:sum1
与 sum2
。虽然由同一个工厂方法返回,但两者是完全不同的。
接着再看如下实例:
react-usecallback - CodeSandboxcodesandbox.ioimport * as React from 'react';
import {
Button, Typography } from 'antd';
type ChildComponentType = {
onChildClickCb?: () => void,
};
const ChildComponent: React.FC = React.memo((props: ChildComponentType) => {
console.log('子组件 ChildComponent');
return (
);
});
const ParentComponent = () => {
const [count, setCount] = React.useState < number > 0;
return (
count:{
count}
{}} />
);
};
export default ParentComponent;
运行,每次单击【+1 按钮】,都会导致 ChildComponent
组件重新渲染:
这里可能会有疑问? ,为什么这里 ChildComponent
已使用 React.memo
包裹。怎么 ParentComponent
的更新会导致 ChildComponent
的更新。
问题出在
语句中,每次 ParentComponent
更新都会传入新的 onChildClickCb
值。就好比如下实例:
{} === {} // false
这里如果想要传入的 onChildClickCb
值不变,可以使 useCallback
进行包裹。在线实例:
import * as React from 'react';
import {
Button, Typography } from 'antd';
type ChildComponentType = {
onChildClickCb?: () => void,
};
const ChildComponent: React.FC = React.memo((props: ChildComponentType) => {
console.log('子组件 ChildComponent');
return (
);
});
const ParentComponent = () => {
const [count, setCount] = React.useState < number > 0;
const onChildClickCb = React.useCallback(() => {}, []); // 使用 useCallback 包裹 onChildClickCb
return (
count:{
count}
);
};
export default ParentComponent;
React.useMemo
React.useMemo
与 React.useCallback
函数签名类似。唯一不同的是 React.useMemo
缓存第一个参数的返回值(nextCreate()
),而 React.useCallback
缓存第一个参数的函数(callback
)。 因此 React.useMemo
常用于缓存计算量密集的函数返回值。
export function useCallback(callback: T, inputs: Array | void | null): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
const nextInputs = inputs !== undefined && inputs !== null ? inputs : [callback];
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
const prevInputs = prevState[1];
if (areHookInputsEqual(nextInputs, prevInputs)) {
return prevState[0];
}
}
workInProgressHook.memoizedState = [callback, nextInputs];
return callback;
}
export function useMemo(nextCreate: () => T, inputs: Array | void | null): T {
currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
workInProgressHook = createWorkInProgressHook();
const nextInputs = inputs !== undefined && inputs !== null ? inputs : [nextCreate];
const prevState = workInProgressHook.memoizedState;
if (prevState !== null) {
const prevInputs = prevState[1];
if (areHookInputsEqual(nextInputs, prevInputs)) {
return prevState[0];
}
}
const nextValue = nextCreate(); // 计算值
workInProgressHook.memoizedState = [nextValue, nextInputs];
return nextValue;
}
workInProgressHook
对象
{
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
areHookInputsEqual() 源码
export default function areHookInputsEqual(arr1: any[], arr2: any[]) {
for (let i = 0; i < arr1.length; i++) {
const val1 = arr1[i];
const val2 = arr2[i];
if (
(val1 === val2 && (val1 !== 0 || 1 / val1 === 1 / (val2: any))) ||
(val1 !== val1 && val2 !== val2) // eslint-disable-line no-self-compare
) {
continue;
}
return false;
}
return true;
}
从源码可以清楚了解到,只会记录上一次的记录。对比算法也是浅层对比。
了解 React.useMemo
工作原理,来实例实践一下:
import * as React from 'react';
import {
Button, Typography } from 'antd';
type ChildComponentType = {
resultComputed?: number[],
};
const ChildComponent: React.FC = React.memo((props: ChildComponentType) => {
console.log('子组件 ChildComponent', props.resultComputed);
return (
);
});
const ParentComponent = () => {
const [count, setCount] = React.useState < number > 0;
const calculator = (num?: number) => {
return [];
};
const resultComputed = calculator();
return (
count:{
count}
);
};
export default ParentComponent;
运行,每次单击【+1 按钮】,都会导致 ChildComponent
组件重新渲染:
问题出在如下代码:
const calculator = (num?: number) => {
return [];
};
const resultComputed = calculator();
每次调用 calculator()
都返回新的 resultComputed
。如果要修复这个问题,这里可以使用 React.useMemo
对 calculator
方法进行包裹。
const resultComputed = React.useMemo(calculator, []);
运行,每次单击【+1 按钮】
本文是作者最近学习的一点心得,与大家分享分享。不过不要 "因为有,强行使用"。只有在发现页面卡顿时,或者性能不好时,可以从这方面入手。原本 React 重新渲染对性能影响一般情况可以忽略不计。因为 memoized 还是需要消耗一定内存的,如果你不正确地大量使用这些优化,可能适得其反哦 。