React Hook学习(useMemo和useCallback)

useMemo和useCallback的区别和使用场景

Featured image

介绍

useCallbackuseMemo 非常相似,所以放在同一章看。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

那么它们的区别是什么?大概来说呢,区别就是 useCallback 缓存函数的引用,useMemo 缓存计算数据的值。

源码分析

为了清楚一点,我们来看下 useCallbackuseMemo 的源码 ReactFiberHooks.new.js:

function updateCallback<T>(
    callback: T, 
    deps: Array<mixed> | void | null
): T {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    const prevState = hook.memoizedState;
    if (prevState !== null) {
        if (nextDeps !== null) {
            const prevDeps: Array<mixed> | null = prevState[1];
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                return prevState[0];
            }
        }
    }
    hook.memoizedState = [callback, nextDeps];
    return callback;
}

function updateMemo<T>(
    nextCreate: () => T,
    deps: Array<mixed> | void | null,
): T {
    const hook = updateWorkInProgressHook();
    const nextDeps = deps === undefined ? null : deps;
    const prevState = hook.memoizedState;
    if (prevState !== null) {
        // Assume these are defined. If they're not, areHookInputsEqual will warn.
        if (nextDeps !== null) {
            const prevDeps: Array<mixed> | null = prevState[1];
            if (areHookInputsEqual(nextDeps, prevDeps)) {
                return prevState[0];
            }
        }
    }
    const nextValue = nextCreate();
    hook.memoizedState = [nextValue, nextDeps];
    return nextValue;
}

const HooksDispatcherOnUpdate: Dispatcher = {
    useCallback: updateCallback,
    useMemo: updateMemo,
    ...
};

细心的已经可以看出二者的不同之处了。

  1. 看下 updateCallback 里这句:
hook.memoizedState = [callback, nextDeps];

缓存的是传入函数的引用。
这样有什么用处呢?

  1. 看下 updateMemo 里这句:
hook.memoizedState = [nextValue, nextDeps];

缓存的是的 传入函数nextCreate()的返回值。

应用场景

useCallback应用

先找一个父子组件的例子,先写parent.js:

import React, { useState } from 'react';
import Child from './child';

const Parent = () => {
    const [count, setCount] = useState(0);
    const clickChild = () => {
        console.log('click Child');
    };

    const clickParent = () => {
        console.log('click Parent');
        setCount(preCount => preCount + 1);
    };

    return (
        <div>
            Count: {count}
            <hr />
            <button onClick={clickParent}>click Parent</button>
            <Child clickChild={clickChild} />
        </div>
    );
};

export default Parent;

然后是child.js

import React, { memo } from 'react';

const Child = memo(({ clickChild }) => {
    console.log('Child render >>');
    return <button onClick={clickChild}>clickChild</button>;
});

export default Child;

我们发现,点击2次「click Parent」按钮,控制台会出现:

click Parent
Child render >>
click Parent
Child render >>

发现虽然采用了memo优化,但是每次Parent重新渲染的时候,Child也跟着重新渲染了,这多浪费资源。
来使用useCallback做如下优化Child的渲染,我们改下parent.js:

import React, { useState, useCallback } from 'react';
import Child from './child';

const Parent = () => {
    const [count, setCount] = useState(0);
    // 代码只改了这里哦 >>
    const clickChild = useCallback(() => {
        console.log('click Child');
    }, []);

    const clickParent = () => {
        console.log('click Parent');
        setCount(preCount => preCount + 1);
    };

    return (
        <div>
            Count: {count}
            <hr />
            <button onClick={clickParent}>click Parent</button>
            <Child clickChild={clickChild} />
        </div>
    );
};

export default Parent;

再次运行点击2次「click Parent」按钮,看下日志:

click Parent
click Parent

已经不会重新渲染Child组件了,性能得到了提升。

useCallback 第2个参数是依赖项,加入依赖项后可以控制在那个参数变化后,渲染Child。这里就不展开了。

useMemo应用

先找个例子看看没使用useMemo的情况:

import React, { useState } from 'react';

const Compute = () => {
    const [count, setCount] = useState(666);
    const [other, setOther] = useState(1);
  
    function computeCount(count) {
        console.log('假设耗时的计算 >>', count);
        return count * count
    }

    const clickUpdateCount = () => {
        setCount(preCount => preCount * 2);
    };

    const clickUpdateOther = () => {
        setOther(other => other + 1);
    };

    const computeValue = computeCount(count);

    return (
        <div>
            Count: {count}
            <br />
            Other: {other}
            <br />
            Result: {computeValue}
            <hr />
            <button onClick={clickUpdateCount}>addCount</button>
            <button onClick={clickUpdateOther}>addOther</button>
        </div>
    );
};

export default Compute;

computeCount(count){} 的入参是count
点击「addCount」按钮为改变count, 会执行到 computeCount()
但是,点击「addOther」按钮,和count没有关系,也会执行到 computeCount() ,这就有多点累赘了,还浪费资源影响性能。

我们用useMemo改造一下:

import React, { useState, useMemo } from 'react';

const Compute = () => {
    const [count, setCount] = useState(666);
    const [other, setOther] = useState(1);
  
    function computeCount(count) {
        console.log('假设耗时的计算 >>', count);
        return count * count
    }

    const clickUpdateCount = () => {
        setCount(preCount => preCount * 2);
    };

    const clickUpdateOther = () => {
        setOther(other => other + 1);
    };

    const computeValue = useMemo(() => computeCount(count), [count]);

    return (
        <div>
            Count: {count}
            <br />
            Other: {other}
            <br />
            Result: {computeValue}
            <hr />
            <button onClick={clickUpdateCount}>addCount</button>
            <button onClick={clickUpdateOther}>addOther</button>
        </div>
    );
};

export default Compute;

运行下看看效果:
点击「addCount」按钮为改变count, 会执行到 computeCount()
但是,点击「addOther」按钮,不会执行到 computeCount() ,性能得以提升。

其他思考

如何测量 DOM 节点?

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。这里有一个 小 demo:

function MeasureExample() {
    const [height, setHeight] = useState(0);

    const measuredRef = useCallback(node => {
        if (node !== null) {
            setHeight(node.getBoundingClientRect().height);
        }
    }, []);

    return (
        <>
            <h1 ref={measuredRef}>Hello, world</h1>
            <h2>The above header is {Math.round(height)}px tall</h2>
        </>
    );
}

在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

在此示例中,仅在安装和卸载组件时才调用回调ref,因为渲染的<h1>组件在所有重新渲染期间均保持存在。 如果您希望在组件调整大小时收到通知,则可能需要使用ResizeObserver或基于其构建的第三方Hook。

如果你愿意,你可以 把这个逻辑抽取出来作为 一个可复用的 Hook:

function MeasureExample() {
    const [rect, ref] = useClientRect();
    return (
        <>
            <h1 ref={ref}>Hello, world</h1>
            {rect !== null &&
                <h2>The above header is {Math.round(rect.height)}px tall</h2>
            }
        </>
    );
}

function useClientRect() {
    const [rect, setRect] = useState(null);
    const ref = useCallback(node => {
        if (node !== null) {
            setRect(node.getBoundingClientRect());
        }
    }, []);
    return [rect, ref];
}