关注 程序员成长指北,回复“1”

加入我们一起学习,天天进步

返回一个 memoized 回调函数。把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

使用 useCallback 的场景?

  1. 函数被 useEffect 内部所使用,但为了避免频繁 useEffect 的频繁调用,所以我包一下;

  2. 我只是为了解决 hooks lint 的提示报警,所以我包一下;

  3. 因为有一个使用了 useCallback 的函数引用了我这个函数,所以我不得不包一下;

  4. 当这个函数会被传递给子组件,为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变;

  5. 需要保存一个函数闭包结果,如配合 debounce、throttle 使用;

  6. 我希望这个useCallback包裹但函数,但某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下;

我们做了投票,发现场景 4 是使用的最多的

案例

假如这里有个文章组件,我想观察当「文章内容」明确后, 用户对「文章标题」的修改频率如何。这个 具体实现:当「文章内容」的长度大于 0 时编辑「文章标题」就上报埋点,同时带上「文章标题」和「文章内容」的 字符长度。

点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/LYbdomv?editors=0010

小胡写出了下面这一段代码,大家可以细看一下,有哪些地方需要优化?它有哪些地方不规范?

// 新建文章组件
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);

注:这段代码是我为了模拟 useEffect 和 useCallback 同时使用而构思出来的,大家不用深究。当然有其他写法可以避免问题,我们这里只是借这个 Demo 描述一下这种类型的场景。

CodeReview 与修改

哪些地方需要优化?

子组件 ArticleTypeSetting 是使用 memo 包裹的,这个组件是希望尽可能的减少渲染次数的(假装这个组件有性能问题,一般不用包)。但是,现在每当修改任意一个值(如 other),子组件都会重新渲染,这显然是没有达到优化的预期的。

哪些地方不规范?

image

这里不规范, useEffect 中使用了 report 函数,但是没有将它放到依赖数组中。我认为这是一件比较危险的事情,在 Hooks 中经常有过期状态的问题。插件已经帮你提示了,虽然现在你自测感觉没问题,但你很难保证在经过几轮轮修改之后,虽然你的代码一堆 warning 或 error,但跑起来没问题。

修改代码

小胡于是对代码进行了一些修改:

  1. 将 getTextLen 和 report 使用 useCallback 包裹

  // 获取当前「标题」和「内容」的长度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 带来的隐式依赖问题」。

如何解决?

方式 1(不推荐):将所有状态都挂到 Ref 上,然后每次修改状态之后主动触发渲染

这种做法确实可以解决这个问题,但是代码不宜维护,理由如下:

  1. 一旦一个组件这样写了之后,之后要有什么新的状态也只好放到这里面。而在新写组件的时候,你不知道什么时候会碰到这个问题,因为一旦碰到了你只有使用 forceUpdate 来解决,要改相关状态的定义,每次使用的时候还要把 someSate 改为 ref.someState。

  2. 每次想更新视图时都需要 forceUpdate,官网是不推荐这种方式的,链接点我

方式 2:将 函数绑定到 useRef 上来解决

  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 上又会绑定最新的函数,不会有闭包问题。我在开发一个复杂项目中,大量的使用了这种方式,这让我的开发效率提升。它让我专注于写业务,而不是专注于解决闭包问题。

这种处理方式的灵感来源于 Dan 的博客:使用 React Hooks 声明 setInterval

优化使用 ref 的体验

虽然把函数挂到 ref 上可以很好到解决这个问题,但是我在开发的时候我并不知道一个函数之后会不会碰到这个闭包问题,但我又不想所以函数全部都这样干。

我对这种方式抽象封装了一下,得到这样一个工具函数,它通过将函数挂到 ref 上,保证永远都是拿到最新状态的函数,往外暴露时使用 useCallback 包裹,保证函数引用不更新。

export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {const callbackRef = useRef(callback);callbackRef.current = callback;return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
}

使用时,简单的把原来有闭包问题的函数包裹一下,不需要传递依赖性,方便简单又好用。

点击链接,打开 console 看Demo:https://codepen.io/huyao/pen/XWNELYr?editors=0010

  // 获取当前「标题」和「内容」的长度const getTextLen = useRefCallback(() => {return [title.length, content.length];});// 上报当前「标题」和「内容」的长度const report = useRefCallback(() => {const [titleLen, contentLen] = getTextLen();if (contentLen > 0) {console.log(`埋点 >>> 内容长度 ${titleLen}, 内容长度${contentLen}`);}});/*** 副作用* 当「标题」长度变化时,上报*/useEffect(() => {report();}, [title, report]);

思考我们最开始使用 useCallback 的理由

我认为其实最开始使用 useCallback 的理由中,只有「需要保存一个函数闭包结果,如配合 debounce、throttle 使用」这个是真正需要使用 useCallback 的,其他的都可能带来风险,比如:

当 useCallback 和 useEffect 组合使用时,由于 useCallback 的依赖项变化也会导致 useEffect 执行,这种隐式依赖会带来BUG或隐患。因为在编程中,函数只是一个工具,但现在一旦某个函数使用了 useCallback ,当这个函数的依赖项变化时所有直接或间接调用这个 useCallback 的都需要回归。所以我说这是成本高、有风险的事情。

而「为了避免子组件频繁渲染,使用 useCallback 包裹,保持引用不变」这种情况下,当 useCallback 的依赖项变化时,函数的引用也在更新,没有完全的避免子组件频繁渲染问题。

而「希望这个useCallback函数的某个依赖项变化时,引用了我这个函数的所有 useEffect 都得重新执行一下」这个理由,我认为它是有风险的,虽有有时候你确实希望这么做,但我认为这样在设计上就不对,副作用怎么调用应该由副作用来决定,不应该由依赖的函数来影响,当你真正碰上这个场景,你应该将所有应该主动的把触发 useEffect 执行的状态都放入依赖数组中。

结论

在绝大多数情况下,开发者想要的仅仅只是避免函数的引用变化而已,而 useCallback 带来的隐式依赖问题会给你带来很大的麻烦,所以推荐使用 useRefCallback,把函数挂到 ref 上,这样代码更不容易留下隐患或带来问题,还可以省去维护 useCallback 依赖项的精力。

而 useRefCallback 本质上就是帮你把函数挂在 ref 上,并方便你使用 ref.current。

export function useRefCallback<T extends (...args: any[]) => any>(callback: T) {const callbackRef = useRef(callback);callbackRef.current = callback;return useCallback((...args: any[]) => callbackRef.current(...args), []) as T;
}

作者:胡耀  原文:https://github.com/huyaocode/webKnowledge/issues/12

❤️爱心三连击

1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号【程序员成长指北】,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。

useCallback 的问题和隐患的解决方案 - 胡耀(字节跳动)相关推荐

  1. 全媒体运营师胡耀文教你:从0到1搭建直播运营体系

    越来越多 To B 企业开始做直播,无论是 SAP/微软/AWS 这样的老牌大厂,还是像很多 SaaS 创新企业,都投入到直播大潮中.疫情爆发后,To B 直播更如雨后春笋般涌现. 很多 To B 企 ...

  2. 全媒体运营师胡耀文教你:产品运营如何进行产品规划?

    最近在摸索八字还没一撇的新业务,还没立项呢,就一直在思考怎么能提高成功率,关键要素是什么,抓耳挠腮地想了好久,但感觉都是一些零散的点. 作为一个在追求确定性与不确定性之间摇摆的人,我总是希望能有一些自 ...

  3. 全媒体运营师胡耀文教你:产品运营生于痛点,死于增长

    对于很多产品来说,前期往往可以抓住市场的机遇和用户的痛点,因此快速发展.但是到了一定的阶段,就很难实现突破,再次增长了. 最近发现一个令人诧异的现象,很多产品由市场的痛点而生,在前期生存得很好,但是到 ...

  4. 字节跳动一站式数据治理解决方案及平台架构

    更多技术交流.求职机会.试用福利,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 "一站式数据治理解决方案及平台架构"的分享会分为四个部分展开: 首先,明确数据治理的 ...

  5. 阿里曾洽谈收购才云科技事宜:被字节跳动截胡,收入囊中

    2020年7月30日,才云科技( Caicloud )宣布被字节跳动收购. 据了解,阿里也与其洽谈过收购事宜,但谈判的关键时刻被字节跳动截胡. 在字节跳动收购完成后,张鑫将作为字节跳动火山引擎云原生业 ...

  6. 全媒体运营师胡耀文教你:如何以0成本短时间内快速获取用户?

    如今的获客成本很高而且获客渠道也很多,对于创业公司来说是比较复杂的.随着互联网的发展,如今很多推广都是通过互联网渠道进行,合理地进行运用也能获得很好的效果. 此次想分享的是产品运营如何短时间内低成本地 ...

  7. 全媒体运营师胡耀文教你:用户运营体系的推导思考

    体系,是一定范围内同类的事物按照秩序联系组合而成的整体.用户运营体系,则是用户需求与企业需求的结合,是面向双方的解决方案. 规划用户运营的体系,其目的为理清业务运作模式及提前做好能力储备,便于后续进行 ...

  8. 胡耀文教你:裂变8级、转化率32%、K值7.4的老带新式分销全复盘

    众所周知,笔者在前段时间发起一个裂变项目--老带新工具箱. 这个项目做得很简单,并没有像野生运营的案例拆解项目,从诞生到现在一共经历十余次的迭代,笔者做它的起因是想测试一下我对于老带新的认识,以及基于 ...

  9. 新媒体运营胡耀文教程:产品运营视阈下的数据分析

    数据分析对于产品运营来说,是一面可以反映业绩情况.检验运营效果的镜子,更是一种工作能力和处事思维.数据分析者的不同职业阶段,也对应着数据分析的不同业务流程. 什么是数据分析? 数据分析是目标,是工具, ...

最新文章

  1. onclick 获取点击之后的img 的id_前端,点击按钮跳出视频带蒙层,且视频永远居于屏幕中间...
  2. 对方不想和你说话 php,对方不想和你聊天的表现,遇到后赶紧放弃
  3. 牛人推荐机器学习网站
  4. 中国中草药提取物市场需求容量与投资价值预测报告2022年
  5. Unity 编译apk启动出异常
  6. 即插即用的轻量注意力机制ECA--Net
  7. java空值转datetime,解决Java (Spring boot) 读取数据库字段,datetime 格式为null,抛出异常 Zero date value prohibited...
  8. jquery插件dataTables(dataTables在显示表格的时候,果然是个好东西,支持排序/搜索/分页/...)
  9. linux批量过去5小时前文件名,Linux批量修改文件名
  10. Mybatis 二级缓存简单示例
  11. mysql链接 及备份
  12. 卡巴斯基离线病毒库升级办法
  13. matlab半波整流怎么做,基于Matlab的单相半波可控整流电路的设计与仿真.doc
  14. K3CLOUD新增用户
  15. 使用linux内核仿真ZNS(zoned namespace SSD)
  16. 巴特沃斯滤波器、切比雪夫、椭圆滤波
  17. Nagios-config
  18. Linux mint 16安装后的种种善后
  19. java计算机毕业设计校友社交系统源码+系统+数据库+lw文档+mybatis+运行部署
  20. **如何在catia工程图中自定义新的制图标准**

热门文章

  1. 你还记得当年上课天天玩 JAVA游戏吗
  2. 如何删除联想lenovo硬盘的隐藏分区
  3. 户外风景拍摄自然风光摄影网站搭建模板
  4. MIUI系统获取短信权限问题
  5. 查询出一班、二班的人数和平均分,并且按照由高到低排序
  6. 充满正能量阳光活的生日祝福语
  7. Thinking in Java之吸血鬼数字
  8. 笔记本电脑dns电脑服务器未响应如何处理,提示dns电脑服务器未响应如何处理?...
  9. 情感分析(判断文章正负向)
  10. 用c#开发Android应用(二)——运行Hello World!