useMemo 与 useCallback 使用

304 阅读4分钟

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

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

useMemo 和 useCallback 函数签名

我们简单的看一下 useMemo 和 useCallback 的函数签名:

useMemo:

export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

useCallback:

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

接下来,我们来看看两者的使用场景:

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 传入一个函数和依赖数组,返回一个缓存的变量值

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

我们先来看一下在组件中不使用 useMemo 会有怎样的问题:

import React, { useState } from "react";
import ReactDOM from "react-dom";
import * as React from "react";
import { useState, useMemo } from "react";
export default function UseMemoPage(props) {
  const [count, setCount] = useState(0);
  const [value, setValue] = useState("");
  const expensive = () => {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 1000; i++) {
      sum += i;
    }
    return sum;
  };
  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"));

这里创建了两个 state,count 和 value,count 在button 点击时用于计数,value 则用于保存 input 的值。然后

通过 expensive 函数执行一次昂贵的计算,拿到 count 对应的某个值。我们在控制台中可以看到,无论是修改 count 还是 value,由于组建的重新渲染,都会触发 expensive 的执行,但是这里的昂贵计算只依赖于 count 的值,在 value 发生改变的时候,是没有必要执行 expensive 计算的,这样就会导致性能损耗的问题。在这种情况下,我们就可以使用 useMemo,只有在 count 的值 修改时,才执行 expensive 计算:

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"));

在上面的代码中,使用了 useMemo,把原先的 expensive 函数和依赖项 count 传入 useMemo,仅在 依赖项 count 变化的时候才会触发 expensive 执行,在 value 变化的时候,返回上一次缓存的值,不会再触发 expensive 执行。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useCallback 传入一个函数和依赖数组,返回一个缓存后的函数。

把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的 memoized 版本, 该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避 免非必要渲染(例如 shouldComponentUpdate )的子组件时,它将非常有用。

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

同样,我们先来看一下在组件中不使用 useCallback 会有怎样的问题:

import * as React from "react";
import ReactDOM from "react-dom";
import { useState, PureComponent } from "react";
export default function UseCallbackPage(props) {
  const [count, setCount] = useState(0);
  const addClick = () => {
    let sum = 0;
    for (let i = 0; i < count; i++) {
      sum += i;
    }
    return sum;
  };
  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)} />
      // count 或 value 变化时都会导致 Child 组件重新渲染
      <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"));

我们定义了一个 UseCallbackPage 组件和 Child 子组件。在 UseCallbackPage 组件中定义了两个 state,count 和 value,还有 Child 子组件的 addClick 方法。在 UseCallbackPage 组件中,将 addClick 作为 props 传递给 Child 子组件。当 value 的值变化时,就会触发 UseCallbackPage 组件更新,同时也会触发 Child 组件更新,也就是 父组件更新了,子组件也会执行更新。但是在大多数场景下,子组件的更新是没有必要的,此时,我们可以借助 useCallback 来返回一个缓存后的函数,然后把这个函数作为 props 传递给 子组件,就可以避免子组件不必要的更新。

import * as React from "react";
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 将 addClick 函数做了缓存处理,然后把 addClick 作为 props 传递给 Child 组件,当 UseCallbackPage 组件更新时,就避免了 Child 组件的更新。

不仅是上面的这个例子,所有依赖本地状态或 props 来创建函数,需要使用到缓存函数的地方,都是 useCallback 的应用场景