面试官:用 useMemo 实现 useCallback 的功能?

2,767 阅读4分钟

useMemo 和 useCallback 是React 16.8 提供 的Hook。在函数组件中,函数组件的每一次调用都会重新执行内部的所有逻辑,就会带来较大的性能损耗。而 useMemo 和 useCallback 可以用来解决函数组件更新过程中的性能问题。

useMemo 和 useCallback 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行。并且这两个 hooks 都是返回缓存的值。useMemo 返回的是缓存的变量,而 useCallback 返回的是缓存的函数。

useMemo 的使用

我们来看一下 useMemo 的使用:

// 使用 useMemo 缓存变量值,只有在依赖变化时,才重新执行
  const expensive = useMemo(() => {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 1000; i++) {
      sum += i;
    }
    return sum;
    //只有count变化,这里才重新执行
  }, [count]);

在上面的代码中,我们在 useMemo 的 "创建"函数中返回了累加后的 sum ,经过 useMemo 的包装后,返回了一个被缓存的变量值,只有在依赖项发现变化时,才会重新执行 "创建"函数,重新计算缓存的变量值。

可以通过下面的例子验证效果:

import React, { useState, useMemo } from "react";
import ReactDOM from "react-dom";
export default function UseMemoPage(props) {
  const [count, setCount] = useState(0);
  // 使用 useMemo 缓存变量值,只有在依赖变化时,才重新执行
  const expensive = useMemo(() => {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 1000; i++) {
      sum += i;
    }
    return sum;
    //只有count变化,这里才重新执行
  }, [count]);
  const [value, setValue] = useState("");
  return (
    <div>
      <h3>UseMemoPage</h3>
      <p>expensive:{expensive}</p>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <input value={value} onChange={event => setValue(event.target.value)} />
    </div>
  );
}

ReactDOM.render(<UseMemoPage />, document.getElementById("container"));

useCallback 的使用

我们再来看 useCallback的使用:

// 使用 useCallback 缓存函数
  const addClick = useCallback(() => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  }, [count]);

在上面的代码中,将一个回调函数传入 useCallback,它会返回一个缓存后的回调函数,只有在依赖项发生改变时该回调函数才会重新执行。

可以通过下面的示例验证效果:

import ReactDOM from "react-dom";
import React, { useState, useCallback, PureComponent } from "react";
export default function UseCallbackPage(props) {
  const [count, setCount] = useState(0);
  // 使用 useCallback 缓存函数
  const addClick = useCallback(() => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  }, [count]);
  const [value, setValue] = useState("");
  return (
    <div>
      <h3>UseCallbackPage</h3>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <input value={value} onChange={event => setValue(event.target.value)} />
      {/*将 使用 useCallback 缓存后的函数传递给子组件,可避免子组件不必要的更新*/}
      <Child addClick={addClick} />
    </div>
  );
}
class Child extends PureComponent {
  render() {
    console.log("child render");
    const { addClick } = this.props;
    return (
      <div>
        <h3>Child</h3>
        <button onClick={() => console.log(addClick())}>add</button>
      </div>
    );
  }
}

ReactDOM.render(<UseCallbackPage />, document.getElementById("container"));

useCallback 返回的是一个缓存后的回调函数,而 useMemo 返回的是一个缓存后的变量。它们两者都是返回一个缓存后的值,仅仅只是返回的值类型不一样而已。那么可不可以将它们的功能互换呢?即useCallback 返回缓存后的变量,useMemo 返回缓存后的回调函数。

我们来看看useMemo 和 useCallback 的源码实现。

useMemo 源码实现

组件首次渲染时 useMemo 的源码实现:

// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function mountMemo<T>(
  nextCreate: () => T, // “创建”函数
  deps: Array<mixed> | void | null, // 依赖项
): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate(); // 执行 "创建"函数
  hook.memoizedState = [nextValue, nextDeps]; // 将 "创建"函数 执行后的返回值缓存起来
  return nextValue; // 返回缓存后的变量值
}

useMemo 的依赖项发生变化时useMemo的源码实现:

// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
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;
}

从源码中可以看到,无论是组件首次渲染还是在依赖项发生变化后,useMemo 都是返回一个缓存后的变量值。

我们注意看这行代码:

const nextValue = nextCreate();

nextCreate 是我们在调用 useMemo 时传递给它的 "创建"函数,React 将 nextCreate 执行后的返回值经过缓存处理后,再将缓存后的值返回。既然 useMemo 返回的是 nextCreate 执行后的返回值,那么我们是不是可以在 nextCreate 中返回一个回调函数,再由useMemo返回这个回调函数,由此来模拟useCallback的功能呢?

下面我们再来看看 useCallback 的源码实现。

useCallback 源码实现

组件首次渲染时 useCallback 的源码实现:

// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

useCallback 的依赖项发生变化时useMemo的源码实现:

// React 版本:16.13.1
// react-reconciler/src/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;
}

我们同样可以从源码中看到,无论是组件首次渲染还是在依赖项发生变化后,useCallback 都是返回一个缓存后的回调函数。

由此可见,useCallback 只能返回一个缓存后的回调函数,它不能模拟useMemo的功能,返回一个缓存后的变量值。

useMemo 模拟 useCallback 的功能

在上文中,我们提到在 useMemo 的 "创建"函数 中返回一个回调函数,来模拟 useCallback的功能,下面我们来验证一下其结果:

在 useMemo 中返回一个回调函数:

const addClick = useMemo(() => 
  return () => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  }
}, [count]);

完整示例:

import ReactDOM from "react-dom";
import React, { useState, useMemo, PureComponent } from "react";
export default function UseCallbackPage(props) {
  const [count, setCount] = useState(0);
  // 使用 useMemo 缓存函数
  const addClick = useMemo(() => () => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  }, [count]);
  const [value, setValue] = useState("");
  return (
    <div>
      <h3>UseCallbackPage</h3>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
      <input value={value} onChange={event => setValue(event.target.value)} />
      {/*将 使用 useCallback 缓存后的函数传递给子组件,可避免子组件不必要的更新*/}
      <Child addClick={addClick} />
    </div>
  );
}
class Child extends PureComponent {
  render() {
    console.log("child render");
    const { addClick } = this.props;
    return (
      <div>
        <h3>Child</h3>
        <button onClick={() => console.log(addClick())}>add</button>
      </div>
    );
  }
}

ReactDOM.render(<UseCallbackPage />, document.getElementById("container"));

通过上面的示例验证,通过 useMemo 返回一个回调函数来模拟useCallback的功能的效果和 useCallback 是完全一致的。

小结

useMemo 和 useCallback 都是返回一个缓存后的值,useMemo 返回的是一个缓存后的变量,而useCallback 返回的是一个缓存后的回调函数。可以在 useMemo 的 "创建函数" 中返回一个回调函数来模拟 useCallback 的功能。useMemo 和 useCallback 可以用来解决函数组件更新过程中的性能问题。