useCallback 的滥用

1,448 阅读2分钟

先放个结论

绝大多数方法不应用 useCallback 包装。

背景

写这篇文章是 2021 年底,相信很多人都在项目中使用了 Hooks,大多数情况还是很爽的,但还是看到了一些文章对于 Hooks 有了比较负面的理解。主要集中在写出 Hooks 好写,但写好要做很多性能优化。

在很多篇文章中看到了这个观点:

Function Component 会在每次 re-render 中重复创建其中声明的 function,故为了避免这部分性能消耗,函数应使用 useCallBack 避免组件 render 时重复创建.

首先肯定这句话是正确的,但问题在于函数创建真的有很大的性能消耗?

useCallback 包裹函数到底优化了什么

直接来看个例子,在此我直接引用了别人文章中的片段。

let num = 0;

function Example () => {
  console.count('render times')
 
  let [count, setCount] = useState(0)

  const handleClick = () => {
    setCount(++count)
  }

  return (
    <>
      <p>count: {count}</p>
      <Button type="primary" onClick={handleClick}>
        Button
      </Button>
    </>
  )
}

上面是很常见的例子,文中的观点是,每次点击重新创建了函数 handleClick,为了性能应做如下改写

const handleClick = useCallback(()=>{
    setCount(++count)
},[ count ])

乍一看好像说的有道理,这样做只要 count 不改变,handleClick 就可以不用重新创建。

但性能优化从来都是个 trade-off,来深入分析下改写后的差异点:

初始化阶段:
  • handleClick 使用 useCallback 创建,内部产生闭包,缓存当前 count = 0 的回调引用。
count不变的更新阶段:
  • useCallback diff dependencies
  • ✅ 判断 count 无变化,直接取缓存的函数
count变化的更新阶段:
  • useCallback diff dependencies
  • ❌ 判断 dependencies 改变,重新生成方法给 handleClick 引用

可以很清楚的看到,改写后增加了很多执行。在大面积使用 useCallback 之前是否应该权衡下此函数增加的性能是否值当。

最后来看下节省下的是哪部分性能开销:

handleClick 函数创建 * render 次数

函数创建的性能消耗

依然先上结论

简单来说当你必须要创建一个函数时,创建函数的性能消耗就不在优化范围内。

函数创建过程解析

目前,我没查询到任何一份准确的函数创建过程,包括在 ECMA-262。我理解是作为一个标准如果对过程细节规定过多,必然会造成实现的束手束脚。找到的结论仅是各大 JS 引擎对函数创建做了不同的但相似的优化。

262.ecma-international.org/6.0/#sec-te…

262.ecma-international.org/6.0/#sec-cr…

本文观点总结

  1. 函数的创建性能消耗可忽略,或不应作为优化点。
  2. 在使用 hooks 时,不应使用 useCallback、挂载 refs、函数嵌套等方式来优化函数创建的性能。
  3. useCallback 的正确使用场景是获得对函数更强的控制。