useCallback/useMemo 的使用误区

在编写 React Hook 代码时,useCallbackuseMemo时常使人感到困惑。尽管咱们知道他们的功能都是作缓存并优化性能,可是又会担忧由于使用方法不正确致使负优化。本文将阐述useCallbackuseMemo在开发中常见的使用方式和误区,并结合源码剖析缘由,知其然并知其因此然。javascript

1.useCallback

1.1 不要滥用useCallback

考察以下示例:html

import React from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    }
    
    return <div onClick={onClick}>Comp组件</div>
}
复制代码

Comp组件自身触发刷新或做为子组件跟随父组件刷新时,咱们注意到onClick会被从新赋值。为了"提高性能",使用useCallback包裹onClick以达到缓存的目的:java

import React, { useCallback } from 'react';

function Comp() {
    const onClick = useCallback(() => {
        console.log('打印');
    }, []);
    
    return <div onClick={onClick}>Comp组件</div>
}
复制代码

那么问题来了,性能到底有没有得到提高?答案是非但没有,反而不如之前了;咱们改写代码的逻辑结构以后,缘由就会很是清晰:react

import React, { useCallback } from 'react';

function Comp() {
    const onClick = () => {
        console.log('打印');
    };
    
    const memoOnClick = useCallback(onClick, []);
    
    return <div onClick={memoOnClick}>Comp组件</div>
}
复制代码

每一行多余代码的执行都产生消耗,哪怕这消耗只是 CPU 的一丁点热量。官方文档指出,无需担忧建立函数会致使性能问题,因此使用useCallback来改造该场景下的组件,咱们并未得到任何收益(函数仍是会被建立),反而其带来的成本让组件负重(须要对比依赖是否发生变化),useCallback用的越多,负重越多。站在 javascript 的角度,当组件刷新时,未被useCallback包裹的方法将被垃圾回收并从新定义,但被useCallback所制造的闭包将保持对回调函数和依赖项的引用。缓存

1.2 useCallback的正确使用方法

产生误区的缘由是useCallback的设计初衷并不是解决组件内部函数屡次建立的问题,而是减小子组件的没必要要重复渲染。实际上在 React 体系下,优化思路主要有两种:markdown

  • 1.减小从新 render 的次数。由于 React 最耗费性能的就是调和过程(reconciliation),只要不 render 就不会触发 reconciliation。
  • 2.减小计算量,这个天然没必要多说。

因此考察以下场景:闭包

import React, { useState } from 'react';

function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = () => {
        setDataB(o => o + 1);
    }
    
    return <div> <Cheap onClick={onClickA}>组件Cheap:{dataA}</div> <Expensive onClick={onClickB}>组件Expensive:{dataB}</Expensive> </div>
}
复制代码

Expensive是一个渲染成本很是高的组件,但点击Cheap组件也会致使Expensive从新渲染,即便dataB并未发生改变。缘由就是onClickB被从新定义,致使 React 在 diff 新旧组件时,断定组件发生了变化。这时候useCabllbackmemo就发挥了做用:函数

import React, { useState, memo, useCallback } from 'react';

function Expensive({ onClick, name }) {
  console.log('Expensive渲染');
  return <div onClick={onClick}>{name}</div>
}

const MemoExpensive = memo(Expensive);

function Cheap({ onClick, name }) {
  console.log('cheap渲染');
  return <div onClick={onClick}>{name}</div>
}

export default function Comp() {
    const [dataA, setDataA] = useState(0);
    const [dataB, setDataB] = useState(0);

    const onClickA = () => {
        setDataA(o => o + 1);
    };
    
    const onClickB = useCallback(() => {
        setDataB(o => o + 1);
    }, []);
    
    return <div>
        <Cheap onClick={onClickA} name={`组件Cheap:${dataA}`}/>
        <MemoExpensive onClick={onClickB} name={`组件Expensive:${dataB}`} />
    </div>
}
复制代码

memo是 React v16.6.0 新增的方法,与 PureComponent 相似,前者负责 Function Component 的优化,后者负责 Class Component。它们都会对传入组件的新旧数据进行浅比较,若是相同则不会触发渲染。工具

因此useCallback保证了onClickB不发生变化,此时点击Cheap组件不会触发Expensive组件的刷新,只有点击Expensive组件才会触发。在实现减小没必要要渲染的优化过程当中,useCallbackmemo是一对利器。运行示例代码oop

1.3 延伸

useCallback源码以下:

// 初始化阶段
function mountCallback(callback, deps) {
    const hook = mountWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    hook.memoizedState = [callback, nextDeps];
    return callback;
}

// 更新阶段
function updateCallback(callback, deps) {
    const hook = updateWorkInProgressHook();,
    const nextDeps = deps === undefined ? null : deps;
    const prevState = hook.memoizedState;
    if (prevState !== null) {
        if (nextDeps !== null) {
            const prevDeps = prevState[1];
            // 比较是否相等
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                // 若是相等,返回旧的 callback
                return prevState[0];
            }
        }
    }
  
    hook.memoizedState = [callback, nextDeps];
    return callback;
}
复制代码

核心逻辑就是比较deps是否发生变化,若是有变化则返回新的callback函数,不然返回原函数。其中比较方法areHookInputsEqual内部实际调用了 React 的is工具方法:

// 排除如下两种特殊状况:
// +0 === -0 // true
// NaN === NaN // false

function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
  );
}
复制代码

2.useMemo

2.1 不要滥用useMemo

import React, { useMemo } from 'react';

function Comp() {
    const v = 0;
    const memoV = useMemo(() => v, []);
    
    return <div>{memoV}</div>;
}
复制代码

建立memoV的开销是没有必要的,缘由与第一节提到的相同。只有当建立行为自己会产生高昂的开销(好比计算上千次才会生成变量值),才有必要使用useMemo,固然这种场景少之又少。

2.2 useMemo的正确使用方法

前文咱们提到,优化 React 组件性能的两个主要思路之一是减小计算量,这也是useMemo的用武之地:

import React, { useMemo } from 'react';

function Comp({ a, b }) {
    const v = 0;
    const calculate = (a, b) => {
        // ... complex calculation
        return c;
    }
    
    const memoV = useMemo((a, b) => v, [a, b]);
    
    return <div>{memoV}</div>;
}
复制代码

3.总结

React Hook 对团队的协做一致性要求很是高,useCallbackuseMemo这一对方法就是很好的示例,更复杂的场景还有对useRef、自定义 Hook 的使用等等。从经验上来看,团队在进行 Hook 编码时须要特别增强 code review,不然很容易出现难以定位的 bug 或性能问题。当前 Hook 的各种方法还不完善,推特上争论也不少,期待 React 后续版本提供出更成熟易用的方案。

相关文章
相关标签/搜索