likes
comments
collection
share

React Hook学习: useCallback、useEventCallback、useCons

作者站长头像
站长
· 阅读数 23

初次使用 React Hook 开发时,可能不怎么会使用 useCallback,以事件回调为例:

const MyComponent: FC = () => {
    // 直接创建函数,不使用 useCallback 包裹
    const handleClick = () => {
        // ...
    };

    return (
        <div>
            <ChildComponent onClick={handleClick} />
        </div>
    );
}

上面示例中,在代码逻辑上,写法自然是正确的,代码运行时,也大概率不会出现问题。

为什么使用 useCallback

在函数组件中,每次渲染时都会重新执行一次函数,因此上例中每次都会新建一个 handleClick 并传递给 ChildComponent 组件。

而对于 ChildComponent 来说,每次渲染时,作为其 props 的 onClick 函数都是新定义的函数,就会导致 ChildComponent 重新渲染。

不必要的渲染情况增多势必会降低网页的性能,那 useCallback 有什么用呢?

useCallback(fn, deps) 写法可以理解为 useMemo(() => fn, deps) ,就是使用 useCallback 可以“记忆”一个函数。也就是说,每次 MyComponent 渲染时,让其中的 handleClick 函数都是同一个函数,这样对于 ChildComponent 来说,其 props.onClick 也就没有变化。

const MyComponent: FC = () => {
    // 使用 useCallback 包裹,每次获得的 handleClick 函数都是同一个
    const handleClick = useCallback(() => {
        // ...
    }, []);

    return (
        <div>
            <ChildComponent onClick={handleClick} />
        </div>
    );
}

使用 useCallback 的问题

既然 useCallback 有优化性能的作用,那么为所有函数都包上 useCallback 不就好了?

首先,代码是给人看的,在代码中充斥大量 useCallback 情况下,会给开发人员增加一定的阅读负担,这种问题对不同人影响程度不同,也会有开发者觉得不可接受,认为是一种过度优化。

其次,和 useMemo 一样的问题,useCallback 使用时需要仔细填写 deps 依赖,一旦写错可能会造成奇怪的 bug,增加了开发工作量,也减少了头发数量。

例如,作为新手开发最为常见的一个问题就是:

const MyComponent: FC = () => {
    const [count, setCount] = useState(0);

    const handleClick = useCallback(() => {
        // 这里获取到的 count 永远都是0,这往往让 React Hook 新手感到迷惑
        setCount(count + 1);
    }, []);

    return (
        <div>
            <ChildComponent onClick={handleClick} />
        </div>
    );
}

新手往往会发现其使用 useCallback 包裹函数中的变量不更新,永远都是初始值。这种情况的一个解决方法就是将 count 作为 deps 依赖之一。

因此 useCallback 的使用可能没有最佳实践,开发者只能在性能、可读性、开发进度间进行平衡取舍。

useCallback 的依赖问题

在实际开发中,可能会出现 deps 依赖十分复杂的情况,比如一个函数可能依赖 4 到 5 个变量的情况,当依赖多了,依赖的更新也更加频繁了,useCallback 的“记忆”效果可能会变差。

官方文档中针对这种情况进行了记录,推荐阅读,也可以看这个 issue ,看看大家针对类似问题的讨论。

官方文档中主要使用名为 useEventCallback 的 hook:

function useEventCallback(fn, dependencies) {
    const ref = useRef(() => {
        throw new Error('Cannot call an event handler while rendering.');
    });

    // 根据依赖去更新 ref ,保证最终调用的函数是最新的
    useEffect(() => {
        ref.current = fn;
    }, [fn, ...dependencies]);

    // useCallback 返回的结果不会改变
    return useCallback(() => {
        const fn = ref.current;
        return fn();
    }, [ref]);
}

即使用 useRef 来保存函数,避免 useCallback 所包裹的函数反复变化的问题。

另外可以查看 material-ui 的实现,也是差不多的原理。

特殊情况 useConstCallback

甚至可以看看一些更特殊的情况:

export function useConstCallback(callback) {
    var ref = useRef(callback);
    return ref.current;
}

const MyComponent: FC = () => {
    const [value, setValue] = useState(false);    

    const setTrue = useConstCallback(function () {
        return setValue(true);
    });
}

其实我理解这和 useCallback(fn, []) 没有区别,但在一些库中也见到这种写法。