useCallback 带来的隐式依赖问题
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // 新建文章组件 function EditArticle() { const [title, setTitle] = useState( "" ); const [content, setContent] = useState( "" ); const [other, setOther] = useState( "" ); // 获取当前「标题」和「内容」的长度 const getTextLen = () => { return [title.length, content.length]; }; // 上报当前「标题」和「内容」的长度 const report = () => { const [titleLen, contentLen] = getTextLen(); if (contentLen > 0) { console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度 ${contentLen}`); } }; /** * 副作用 * 当「标题」长度变化时,上报 */ useEffect(() => { report(); }, [title]); return ( <div className= "App" > 文章标题 <input value={title} onChange={(e) => setTitle(e.target.value)} /> 文章内容 <input value={content} onChange={(e) => setContent(e.target.value)} /> 其他不相关状态: <input value={other} onChange={(e) => setOther(e.target.value)} /> <MemoArticleTypeSetting getTextLen={getTextLen} /> </div> ); } enum ArticleType { WEB = "前端" , SERVER = "后端" , } // 子组件,修改文章类型(无需关注,它只是接受了父组件的一个参数而已) const ArticleTypeSetting: FC<{ getTextLen: () => number[] }> = ({ getTextLen }) => { console.log( " --- ArticleTypeSetting 组件重新渲染 --- " ); const [articleType, setArticleType] = useState<ArticleType>(ArticleType.WEB); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setArticleType(e.target.value as ArticleType); console.log( "埋点 >>> 切换类型,当前「标题」和「内容」长度:" , getTextLen() ); }; return ( <div> <div>文章类型组件,当选择类型时上报「标题」和「内容」长度</div> <div> {[ArticleType.WEB, ArticleType.SERVER].map((type) => ( <div> <input type= "radio" value={type} checked={articleType === type} onChange={handleChange} /> {type} </div> ))} </div> </div> ); }; const MemoArticleTypeSetting = memo(ArticleTypeSetting); |
哪些地方需要优化
- 子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。
- 这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。
对代码进行了一些修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 获取当前「标题」和「内容」的长度 const getTextLen = useCallback(() => { return [title.length, content.length]; }, [title, content]); // 上报当前「标题」和「内容」的长度 const report = useCallback(() => { const [titleLen, contentLen] = getTextLen(); if (contentLen > 0) { console.log(`埋点 >>> 内容长度 ${titleLen}, 内容长度 ${contentLen}`); } }, [getTextLen]); /** * 副作用 * 当「标题」长度变化时,上报 */ useEffect(() => { report(); }, [title, report]); |
还有问题
当 「文章内容」修改了之后,会触发 useEffect 继续上报
为什么
初衷只是使用 useCallback 避免频繁调用,但当一个 useCallback 的依赖项变化后,这个 useEffect 会被执行,就像上面修改过后的代码一样,「文章内容」修改了之后,也会触发 useEffect 的,这就是「useCallback 带来的隐式依赖问题」。
如何解决
将 函数绑定到 useRef 上来解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const getTextLenRef = useRef<() => [number, number]>(() => [0, 0]); // 获取当前「标题」和「内容」的长度 getTextLenRef.current = () => { return [title.length, content.length]; }; // 上报当前「标题」和「内容」的长度 const report = () => { const [titleLen, contentLen] = getTextLenRef.current(); if (contentLen > 0) { console.log(`埋点 >>> 标题长度 ${titleLen}, 内容长度 ${contentLen}`); } }; /** * 副作用 * 当「标题」长度变化时,上报 */ useEffect(() => { report(); }, [title]); |
将函数绑定到 Ref上,ref 引用不论怎样都保持不变,而且函数每次 render ref 上又会绑定最新的函数,不会有闭包问题。我在开发一个复杂项目中,大量的使用了这种方式,这让我的开发效率提升。它让我专注于写业务,而不是专注于解决闭包问题。
思考我们最开始使用 useCallback 的理由
只有「需要保存一个函数闭包结果,如配合 debounce、throttle 使用」这个是真正需要使用 useCallback 的,其他的都可能带来风险
在绝大多数情况下,开发者想要的仅仅只是避免函数的引用变化而已,而 useCallback 带来的隐式依赖问题会给你带来很大的麻烦,所以推荐使用 useRefCallback
,把函数挂到 ref 上,这样代码更不容易留下隐患或带来问题,还可以省去维护 useCallback 依赖项的精力
https://github.com/huyaocode/webKnowledge/issues/12
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!