如何使用usememo和usecallback

2,687 阅读3分钟

你可能在项目中使用过useCallback,也可能在纠结要不要使用useMemo;本文试验几种这两个api的场景,让我们明白该如何使用。先从useMemo开始。

useMemo

useMemo接收两个参数,第一个参数是个函数,第二个参数是依赖项,返回值是函数的结果;只有在依赖项变化时函数才会重新执行。下面这个来自官网的例子说明,useMemo通常用于缓存花销大的计算。

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

下面这个例子expensive用useMemo缓存,expensive2没有;当我们点击按钮,count改变,这时候expensive和expensive2都重新执行。这结果正常,普通函数在每次渲染时都重新执行;useMemo函数在依赖项变化时重新执行。

但是当我们在input中输入时,这时候expensive2重新执行,而expensive就不会,实现了缓存的结果。

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

export default function MemoExpensive() {
  const [count, setCount] = useState(1);
  const [val, setValue] = useState("");

  const expensive = useMemo(() => {
    console.log("memo compute");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  }, [count]);

  const expensive2 = () => {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  };

  return (
    <div>
      <h4>
        {count}-{expensive}
      </h4>
      <h4>
        {count}-{expensive2()}
      </h4>
      {val}
      <div>
        <button onClick={() => setCount(count + 1)}>click me</button>
        <input value={val} onChange={e => setValue(e.target.value)} />
      </div>
    </div>
  );
}

usememo-expensive.gif

既然useMemo能缓存昂贵计算,你肯定会想,那能不能所有数据都缓存,这样性能最好。这篇文章 的作者经过试验,得出结论:缓存也有代价,不是所有场景都适合缓存。适合使用useMemo除了Computationally expensive calculations,还有Referential equality,即在父组件传递给子组件时,参数是引用数据类型,例子说明:

import React, { useCallback, useMemo, useEffect, useState } from "react";

function StringFoo({ bar, baz }) {
  useEffect(() => {
    const options = { bar, baz };
    console.log("string", options);
  }, [bar, baz]);
  return <div>StringFoo</div>;
}

function ObjectFoo({ bar, baz }) {
  useEffect(() => {
    const options = { bar, baz };
    console.log("obj", options);
  }, [bar, baz]);
  return <div>ObjectFoo</div>;
}

function MemoFoo({ bar, baz }) {
  useEffect(() => {
    const options = { bar, baz };
    console.log("memo", options);
  }, [bar, baz]);
  return <div>memoFoo</div>;
}

function Blub() {
  const [count, setCount] = useState(0);

  // object
  const objectBar = () => {};
  const objectBaz = [1, 2, 3];

  // memo
  const memoBar = useCallback(() => {}, []);
  const memoBaz = useMemo(() => [1, 2, 3], []);

  return (
    <>
      <button
        onClick={() => {
          setCount(c => c + 1);
        }}
      >
        {count}
      </button>
      <StringFoo bar="bar" baz="baz" />
      <ObjectFoo bar={objectBar} baz={objectBaz} />
      <MemoFoo bar={memoBar} baz={memoBaz} />
    </>
  );
}

export default Blub;

有三种情况,参数是string;参数是数组和函数;参数是useMemo包裹的数组和函数。当我们点击button时:

  • stringFoo不会重新渲染,
  • ObjectFoo会重新渲染,bar和baz是引用类型,虽然值没变,但是与原先不一样,故会重新渲染
  • MemoFoo不会重新渲染,这就是useMemo起作用的地方

看到在打印台只有第二种情况"obj"变打印出来,你可以点击这里,自己试验

useCallback

useCallback与useMemo作用类似,不同的是useMemo返回的是该函数的结果,而useCallback返回的是该函数。除了函数从父组件传递到子组件可以用useCallback之外,通常在useEffect中也会使用useCallback。好处是这函数可以在其他地方复用;函数只关心和函数本身相关的参数(更加内聚),语义清晰。以下为伪代码。

// 没用useCallback
function Blub() {

  useEffect(() => {
    AuthAPI.update(params).then(data => {
      //  ...
    })
  }, [pathname, params])

}

// 使用了useCallback

function Blub() {

  const authUpdateCb = (() => {
    AuthAPI.update(params).then(data => {
      ...
    })
  }, [params])

  useEffect(() => {
    authUpdateCb()
  }, [pathname, authUpdateCb])

}

总结

useMemo用于昂贵计算和引用类型参数传递的,useCallback与useMemo类似,更多用于将函数包裹,使得该函数只受到它自己依赖项的影响。

寒风卷起,落叶抱冬日。感谢阅读,不对之处请指出。

参考资料

juejin.cn/post/684490…

kentcdodds.com/blog/usemem…

reactjs.org/docs/hooks-…

zhuanlan.zhihu.com/p/66166173