跳到主要内容

优化

React 如何减少不必要的 re-Render

  • 一、shouldComponentUpdate 和 PureComponent
  • 二、函数式组件没有 shouldComponentUpdate,所以可以封装一个高阶组件来判断是否要 render 相当于自己做了 React.memo
  • 三、React.memo 高阶函数
  • 四、immutable

函数式组件优化

使用 react.memo 缓存组件

  • react.memo:本身是一个高阶组件,是 React v16.6 引进来的新属性。它的作用和 React.PureComponent 类似,是用来控制函数组件的重新渲染的。

默认进行 props 浅比较,也可以手动传入返回 boolean 的函数

const Funcomponent = () => {
return <div>这里是优化的函数式组件</div>;
};
function areEqual(prevProps, nextProps) {
// 如果想要组件不render,则返回true,否则返回false
return false;
}
// 若不入第二个从参数,则默认使用浅比较
const MemodFuncComponent = React.memo(FunComponent, areEqual);

useMemo 缓存数据

  • 上面 React.memo() 的使用我们可以发现,最终都是在最外层包装了整个组件,并且需要手动写一个方法比较那些具体的 props 不相同才进行 re-render。而在某些场景下,我们只是希望 component 的部分不要进行 re-render,而不是整个 component 不要 re-render,也就是要实现局部 Pure 功能。
  • 基本用法如下:返回的是一个 memoized 值,只有当依赖项(比如上面的 a,b 发生变化的时候,才会重新计算这个 memoized 值)。memoized 值不变的情况下,不会重新触发渲染逻辑。如果没有提供依赖数组(上面的 [a,b])则每次都会重新计算 memoized 值,也就会 re-redner。
  • useMemo() 是在 render 期间执行的,所以不能进行一些额外的副操作,比如网络请求等。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useCallback 缓存函数

  • 我们知道,如果我们在子组件上写了一个匿名函数,那么父组件每一次 render 都会重新生成一个新的匿名函数,必然会触发子组件的渲染。为了规避这个问题,我们可以使用 useCallback。
  • Callback 的主要作用就是和 React.memo 一起使用,避免子组件的重复渲染

防止 child 组件因为匿名函数的重新声明就重新渲染了 但是函数显然是不会有变化的对吧

// 父组件
const handleChildrenCallback = useCallback(() => {
handleChildren();
}, []);
// 子组件
<ChildrenComponent handleChildren={handleChildrenCallback} />;

慎用 useMemo & useCallback

useMemo 和 useCallback 可以缓存信息,提升性能。但是在某些情况下,滥用这两个方法反而会降低性能。这是因为,轻量级的数据和方法做缓存处理可能比重新渲染更加消耗性能,而缓存处理通常在组件创建的时候执行,这就导致初次渲染组件的速度会降低,减慢了页面首次打开的速度,得不偿失。

推荐仅在以下几种情况下使用 useMemo / useCallback

  1. 在使用 Context Provider 时,使用 useMemo。Provider 通常会有很多组件订阅它的变更,Provider value 引用更新,导致所有订阅此 Provider 的 Consumer 组件及 useContext 所在的组件重绘

  2. 当你用到一个耗时的计算,而此计算的输入是一个在重绘时引用不会变更的变量时,使用 useMemo。比如:数据量较大的 map/filter 操作

  3. 当引用对象作为 useEffect 的依赖时,为了避免不停执行 useEffect 中的函数,需要使用 useMemo。

function App() {
const onChange = useCallback(() => {
...
}, [xxx]);

useEffect(() => {
onChange();
}, [onChange]);

return (
<Input onChange={onChange} />
);
}
  1. 当子组件通过 memo 进行优化时
React.memo(function Input(props) {
return <input onChange={props.onChange} />;
});

// 正确示例
function App() {
const onChange = useCallback(() => {
...
}, [xxx]);

return (
<Input onChange={onChange} />
);
}

useCallback 误区

  1. useCallback 的依赖没有添加,导致代码中随处可见的飘红飘黄警告
  2. 不必要的场景都要加上它导致大家要不停加依赖,增加了心智负担
  3. 在别人封装的组件中暴露出来的回调用 useCallback 包裹可能导致闭包问题导致问题难以排查
  4. useCallback 会对包裹内容进行缓存,触发 render 时避免重复创建,但是也会增加内存使用不能释放,有时候可能是负优化,可参考https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render

关于如何避免依赖问题导致飘红

建议参考这篇文章https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

哪些场景不适合使用 useCallback

前置知识,在 react 中父组件的 render 会导致所有子组件的 render(React.memo 包裹的除外),因此组件中的所有代码都要被执行一次,而通过 useCallback,useMemo 等包裹的内容则根据依赖的浅对比选择性重新生成,这也是大家滥用的主要原因。

对于大部分常见组件回调都不应该使用 useCallback

以下错误示例代码增加了依赖的心智负担,而大部分简单函数的缓存并不会提升什么代码性能,还可能会导致负优化问题。

对于自定义组件(多层级嵌套均无 memo 优化)的回调

以下自定义 Input 组件没有通过 memo 进行封装,因此 App 的每次 render 也会导致 Input 的每次 render,useCallback 并不能阻止 Input 的重新渲染

function Input(props) {
return <input onChange={props.onChange} />;
}

// 错误示例, 用了useCallback, 但是和没用效果是一样的,不如不用
function App() {
const onChange = useCallback(() => {
...
}, [xxx]);

return (
<Input onChange={onChange} />
);
}

// 正确示例
function App() {
const onChange =() => {
...
};

return (
<Input onChange={onChange} />
);
}

对于 useCallback 缺少依赖带来的闭包问题(还是心智负担的问题)

    const [count, setCount] = useState(0);
// 缺少count依赖,导致只能拿到 0
const onChange = useCallback(() => {
console.log(count);
...
}, []);

对于子组件要依赖父组件的回调函数时应该怎么做(一般我们是不希望对传进的函数做依赖的,但是又会飘红)


// 错误示例
// 虽然保证了依赖,但是不能保证使用组件的人一定通过useCallback包裹了onChange,会导致无效依赖
function Input(props) {
useEffect(() => {
...
}, [props.onChange]);
return <input onChange={props.onChange} />;
}

// 正确示例
// 既解决了依赖问题,又不用担心闭包问题
function Input(props) {
const callbackRef = useRef({
onChange: props.onChange
});
callbackRef.current = {
onChange: props.onChange
};

useEffect(() => {
// 通过ref, 绕开空白依赖的报错
callbackRef.current.onChange();
...
}, []);
return <input onChange={props.onChange} />;
}

function App() {
const [count, setCount] = useState(0);
const onChange =() => {
console.log(count);
...
};

return (
<Input onChange={onChange} />
);
}