我有一支技术全面、经验丰富的小型团队,专注高效交付中等规模外包项目,有需要外包项目的可以联系我
很多代码库到处都是 useCallback
/ useMemo
。初衷是好的:减少不必要的重新渲染 、稳定引用 、提速 。然而,用错场景 或铺天盖地地包一层 ,往往只会带来样板代码、脆弱的依赖、以及几乎没有的收益。下面把常见误区、根因与替代方案一次讲清。
为什么大家老是手伸向 useCallback / useMemo?
-
想避免重复渲染:组件反复 render 看起来"慢",于是希望"让引用稳定"来触发更少的 diff。
-
函数/对象每次 render 都是新引用 :担心子组件比较不过,索性全都包一层。
-
期望优化,但缺少度量 :没有 Profile 前置验证,拍脑袋优化极易南辕北辙。
然而,并非所有函数都需要"稳定" ,尤其是子组件没有被 React.memo
包裹 的时候;此外,依赖数组里塞不稳定的对象/函数 ,又会让 effect 次次触发------这就是陷阱的源头。
陷阱一:事件处理器的"过度稳定化"
很多人会无差别地把事件处理器包上 useCallback
,期望"更稳更快"。但只有当子组件是 React.memo
,稳定引用才有意义;否则根本不参与比较。
go
// ❌ 不要这样把临时对象和内联函数传给已 memo 的组件
function Meh() {
return (
<MemoizedComponent
value={{ hello: 'world' }}
onChange={(result) => console.log('result')}
/>
)
}
// ✅ 需要稳定引用时,再用 useMemo / useCallback
function Okay() {
const value = useMemo(() => ({ hello: 'world' }), [])
const onChange = useCallback((result) => console.log(result), [])
return <MemoizedComponent value={value} onChange={onChange} />
}
再看一个常见写法:
go
function MyButton() {
const onClick = useCallback(
(event) => console.log(event.currentTarget.value),
[]
)
return <button onClick={onClick} />
}
这里按钮没有 被 React.memo
包裹,因此传入的 onClick
是否"稳定"并不会改变渲染行为 。而 useCallback
本身也要参与一次创建/比对,徒增复杂度 ------收益≈0。
小结:只有当"接收方"基于引用做浅比较(如
React.memo
)时,稳定才有意义;否则纯属样板。
陷阱二:依赖数组 + Props,极易触发"连环反应"
当某个函数/对象被放入 useEffect
的依赖数组,React 会用 Object.is
做浅比较。只要引用不稳定 ,effect 就会每次 render 都重跑 ,从而抵消掉你以为的"稳定化"。
go
function OhNo({ onChange }) {
const handleChange = useCallback((e: React.ChangeEvent) => {
trackAnalytics('changeEvent', e)
onChange?.(e)
}, [onChange])
return <SomeMemoizedComponent onChange={handleChange} />
}
// 调用方:
<OhNo onChange={() => props.doSomething()} />
调用方把一个临时箭头函数 传了进来,onChange
引用每次都不同 ,于是 handleChange
也被迫每次重建 ;结果是------你以为稳定了,其实全白搭。
再看一个更"真"的例子(热键):
go
export function useHotkeys(hotkeys: Hotkey[]) {
const onKeyDown = useCallback(() => { /* ... */ }, [hotkeys])
useEffect(() => {
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
}
如果 hotkeys
是调用方每次都新建的数组 ,那 onKeyDown
就一定每次变化 ,事件监听也会每次解绑/重绑 。把"稳定"的责任推给所有调用方 ,不仅脆弱,还难以排错。
结论:在依赖链里塞"会变"的引用,再多的 useCallback 都救不了;这不是性能优化,而是"反应式地雷"。
更靠谱的做法:Ref 持久化 + 渐进更新
一个实战稳定 的模式是:用 ref
持有最新值 ,在 effect 中只绑定一次 ,处理逻辑里读取 ref 。这样既保证处理器稳定 ,又能拿到最新数据。
go
export function useHotkeys(hotkeys: Hotkey[]) {
// 1) 用 ref 持久化数据
const hotkeysRef = useRef(hotkeys)
// 2) 每次 render 同步最新值(不加依赖,始终最新)
useEffect(() => {
hotkeysRef.current = hotkeys
})
// 3) 稳定的处理器,不依赖外部变化
const onKeyDown = useCallback((e: KeyboardEvent) => {
const latest = hotkeysRef.current
// ... 用 latest 做判断/匹配
}, [])
// 4) 只绑定一次监听器
useEffect(() => {
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [])
}
-
因此,事件监听不会反复重绑;
-
同时 ,回调里读到的是最新
hotkeys
; -
尽管如此 ,外层组件怎么传都不再牵动内部结构。
很多库(例如 React Query )在内部就采用类似思路:ref 驱动"读最新" ,effect 驱动"只绑定一次"。
React 19 的原生解法:useEffectEvent
(提案)
React 计划提供 useEffectEvent
来表达"非反应式事件" :回调引用稳定 ,内部总能读到最新值 ,且不会把依赖向外蔓延。
go
export function useHotkeys(hotkeys: Hotkey[]) {
// onKeyDown 本身稳定,但其内部每次读取到的都是"最新 hotkeys"
const onKeyDown = useEffectEvent((e: KeyboardEvent) => {
// 使用 hotkeys,始终为最新
})
useEffect(() => {
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [])
}
-
于是 ,不必再手搓
ref
; -
而且,避免了"依赖数组失控";
-
最后 ,事件回调既稳定 又不陈旧,语义清晰。
什么时候该用,什么时候真别用?
可以用的场景(有实打实回报):
-
向
React.memo
子组件传参 :确实需要稳定引用避免无意义重渲。 -
昂贵计算的结果缓存 :
useMemo
封装重活 ,有测量再上。 -
稳定的订阅/解绑回调 :配合
ref
/useEffectEvent
,只绑一次。
应当避免的用法(基本白忙活):
-
给未 memo 化 的子组件一律包 useCallback。
-
把不稳定的对象/函数 塞进依赖数组,导致effect 每次触发。
-
没有基准测试 就"凭感觉"到处加
useMemo
/useCallback
。
简单决策树:
-
子组件 不是
React.memo
→ 别为了"稳定"而稳定。 -
依赖链中存在临时引用 → 用
ref
/useEffectEvent ,消除连锁依赖。 -
计算确实重 且被复用 →
useMemo
,并用 Profile 证明。
关键要点回看
-
useCallback
不是银弹 ;在缺少React.memo
或依赖不稳时,它只会徒增复杂度。 -
依赖数组的稳定性 > 回调的"看起来稳定" ;错位的稳定 会让 effect 每次重跑。
-
Ref + 一次性绑定 (或
useEffectEvent
)是事件类场景的更健壮模式。 -
先测量再优化 :用 DevTools Profiler/
why-did-you-render
等工具,用数据说话 ,再决定是否上useMemo
/useCallback
。
Final Takeaway
今天系统梳理了为何 useCallback
/useMemo
常常事与愿违 ,以及如何用 ref
持久化 + 一次性绑定 (或即将到来的 useEffectEvent
)更直接地解决问题 。优化要以稳定的依赖链为前提,而不是到处包一层"看起来更专业"的 Hook。
前端AI·探索:涵盖动效、React Hooks、Vue 技巧、LLM 应用、Python 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。

最后: