waltcow / blog

A personal blog
21 stars 2 forks source link

useMemo 和 useCallback 使用备忘 #45

Open waltcow opened 4 years ago

waltcow commented 4 years ago

性能优化总是会有成本,但并不总是带来好处。 下面谈谈 useMemouseCallback 的成本和收益。

说说useCallback

结合下面的例子来讲讲

function CandyDispenser() {
  const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
  const [candies, setCandies] = React.useState(initialCandies)
  const dispense = candy => {
    setCandies(allCandies => allCandies.filter(c => c !== candy))
  }
  return (
    <div>
      <h1>Candy Dispenser</h1>
      <div>
        <div>Available Candy</div>
        {candies.length === 0 ? (
          <button onClick={() => setCandies(initialCandies)}>refill</button>
        ) : (
          <ul>
            {candies.map(candy => (
              <li key={candy}>
                <button onClick={() => dispense(candy)}>grab</button> {candy}
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  )
}

当被问到如何优化以上代码时, 很多人会想到用 React.useCallback 里包裹 dispense 函数

const dispense = React.useCallback(candy => {
  setCandies(allCandies => allCandies.filter(c => c !== candy))
}, [])

但实际情况是 使用原来的代码性能会更好, 如果你选择的是 useCallback,再好好思考下。 为什么使用了useCallback 后性能反而性能不如预期呢?!

很多情况使用 React.useCallback 可以用来提高性能,并且“内联函数可能会对性能造成问题”,那么为啥不使用 usecallCallback变得更好的。useCallback版本做了更多的工作之外,它们完全相同。 我们不仅需要定义函数,还要定义一个数组([])并调用 React.useCallback,它本身会设置属性和运行逻辑表达式等。

在组件的第二次渲染中,原来的 dispense 函数被垃圾收集(释放内存空间),然后创建一个新的 dispense 函数。 但是使用 useCallback 时,原来的 dispense 函数不会被垃圾收集,并且会创建一个新的 dispense 函数,所以从内存的角度来看,这会变得更糟。

作为一个相关的说明,如果你有其它依赖,那么React很可能会挂起对前面函数的引用,因为 memoization 通常意味着我们保留旧值的副本,以便在我们获得与先前给出的相同依赖的情况下返回。 特别聪明的你会注意到,这意味着React还必须挂在对这个等式检查依赖项的引用上

再说说useMemo

useMemo 类似于 useCallback,除了它允许你将 memoization 应用于任何值类型(不仅仅是函数)。 它通过接受一个返回值的函数来实现这一点,然后只在需要检索值时调用该函数(通常这只有在每次渲染中依赖项数组中的元素发生变化时才会发生一次)

如果不想在每次渲染时初始化那个 initialCandies 数组, 可以用 useMemo 进行调整

- const initialCandies = ['snickers', 'skittles', 'twix', 'milky way']
+ const initialCandies = React.useMemo(
+  () => ['snickers', 'skittles', 'twix', 'milky way'],
+  [],
+ )

可以避免那个问题,但是节省的成本是如此之小,以至于换来使代码更加复杂的成本是不值得的。实际上,这里使用useMemo 也可能会更糟,因为我们再次进行了函数调用,并且代码会执行属性赋值等。

什么时候使用 useMemo 和 useCallback

这两个 hooks 内置于 React 都有特别的原因:

  1. 引用相等
  2. 昂贵的计算