先放个结论
绝大多数方法不应用 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
diffdependencies
- ✅ 判断
count
无变化,直接取缓存的函数
count变化的更新阶段:
- ❌
useCallback
diffdependencies
- ❌ 判断
dependencies
改变,重新生成方法给handleClick
引用
可以很清楚的看到,改写后增加了很多执行。在大面积使用 useCallback
之前是否应该权衡下此函数增加的性能是否值当。
最后来看下节省下的是哪部分性能开销:
handleClick
函数创建 * render
次数
函数创建的性能消耗
依然先上结论
简单来说当你必须要创建一个函数时,创建函数的性能消耗就不在优化范围内。
函数创建过程解析
目前,我没查询到任何一份准确的函数创建过程,包括在 ECMA-262。我理解是作为一个标准如果对过程细节规定过多,必然会造成实现的束手束脚。找到的结论仅是各大 JS 引擎对函数创建做了不同的但相似的优化。
262.ecma-international.org/6.0/#sec-te…
262.ecma-international.org/6.0/#sec-cr…
本文观点总结
- 函数的创建性能消耗可忽略,或不应作为优化点。
- 在使用 hooks 时,不应使用 useCallback、挂载 refs、函数嵌套等方式来优化函数创建的性能。
- useCallback 的正确使用场景是获得对函数更强的控制。