【AICon 全球人工智能与大模型开发与应用大会】改变 AI 时代下写代码的模式 >>> 了解详情
写点什么

不要再滥用 useMemo 了!你应该重新思考 Hooks memoization

  • 2019-08-22
  • 本文字数:4851 字

    阅读完需:约 16 分钟

不要再滥用useMemo了!你应该重新思考Hooks memoization

在使用 React Hooks 的过程中,作者发现过渡频繁的应用 useMemo 用会影响程序的性能。在本文中作者将与大家分享如何避免过度使用 useMemo,使程序远离性能问题。


经过观察总结,我发现在两种情况下 useMemo 要么没什么用,要么就是用得太多了,而且可能会影响应用程序的性能表现。


第一种情况很容易就能推断出来,但是第二种情况就比较隐蔽了,很容易被忽略。如果你在生产环境的应用程序中使用了 Hook,那么你就可能会在这两个场景中使用 useMemo Hook。


下面我就会谈一谈为什么这些 useMemo 没什么必要,甚至可能影响你的应用性能。此外我会教大家在这些场景中避免过度使用 useMemo 的方法。


我们开始吧。

不需要 useMemo 的情况

为了方便,我们把这两类场景分别称为狮子和变色龙。



先不用纠结为什么这么叫,继续读下去就是。


当你撞上一头雄狮,你的第一反应就是撒丫子跑,不要成为狮子的盘中餐,然后活下来跟别人吹牛。这时候可没空思考那么多。


这就是场景 A。它们是狮子,你应该下意识地躲开它们。


但在谈论它们之前,我们先来看看更隐蔽的变色龙场景。

相同的引用和开销不大的操作

参考下面的示例组件:


/**   @param {number} page   @param {string} type **/const myComponent({page, type}) {   const resolvedValue = useMemo(() => {     getResolvedValue(page, type)  }, [page, type])
return <ExpensiveComponent resolvedValue={resolvedValue}/> }
复制代码


如上所示,显然作者使用了 useMemo。这里他们的思路是,当对 resolvedValue 的引用出现更改时,他们不想重新渲染 ExpensiveComponent。


虽说这个担忧是正确的,但无论何时要用 useMemo 之前都应该考虑两个问题。


  • 首先,传递给 useMemo 的函数开销大不大?在上面这个示例中就是要考虑 getResolvedValue 的开销大不大?JavaScript 数据类型的大多数方法都是优化过的,例如 Array.map、Object.getOwnPropertyNames()等。如果你执行的操作开销不大(想想大 O 符号),那么你就不需要记住返回值。使用 useMemo 的成本可能会超过重新评估该函数的成本。

  • 其次,给定相同的输入值时,对记忆(memoized)值的引用是否会发生变化?例如在上面的代码块中,如果 page 为 2,type 为“GET”,那么对 resolvedValue 的引用是否会变化?简单的回答是考虑 resolvedValue 变量的数据类型。如果 resolvedValue 是原始值(如字符串、数字、布尔值、空值、未定义或符号),则引用就不会变化。也就是说 ExpensiveComponent 不会被重新渲染。


修正过的代码如下:


/**   @param {number} page   @param {string} type **/const MyComponent({page, type}) {  const resolvedValue = getResolvedValue(page, type)  return <ExpensiveComponent resolvedValue={resolvedValue}/> }
复制代码


如前所述,如果 resolvedValue 返回一个字符串之类的原始值,并且 getResolvedValue 这个操作的开销没那么大,那么这段代码就非常合理,效率够高了。


只要 page 和 type 是一样的,比如说没有 prop 更改,resolvedValue 的引用就会保持不变,只是返回的值不是原始值了(例如变成了对象或数组)。


记住这两个问题:要记住的函数开销很大吗,返回的值是原始值吗?每次都思考这两个问题的话,你就能随时判断使用 useMemo 是否合适。

出于多种原因需要记住默认状态

参考以下代码块:


/**   @param {number} page   @param {string} type **/const myComponent({page, type}) {   const defaultState = useMemo(() => ({    fetched: someOperationValue(),    type: type  }), [type])
const [state, setState] = useState(defaultState); return <ExpensiveComponent /> }
复制代码


有人会觉得上面的代码没什么问题,但这里 useMemo 调用肯定是没什么意义的。


首先我们来试着理解一下这段代码背后的思想。作者的思路很不错。当 type prop 更改时他们需要新的 defaultState 对象,并且不希望在每次重新渲染时都引用 defaultState 对象。


虽说这些问题都很实际,但这种方法是错误的,违反了一个基本原则:useState 是不会在每次重新渲染时都重新初始化的,只有在组件重载时才会初始化。


传递给 useState 的参数改名为 INITIAL_STATE 更合理。它只在组件刚加载时计算(或触发)一次。


useState(INITIAL_STATE)
复制代码


虽然作者担心在 useMemo 的 type 数组依赖项发生更改时获取新的 defaultState 值,但这是错误的判断,因为 useState 忽略了新计算的 defaultState 对象。


懒惰初始化 useState 时也是一样的道理,如下所示:


/**   @param {number} page    @param {string} type **/const myComponent({page, type}) {  // default state initializer   const defaultState = () => {    console.log("default state computed")    return {       fetched: someOperationValue(),       type: type    }  }
const [state, setState] = useState(defaultState); return <ExpensiveComponent /> }
复制代码


在上面的示例中,defaultState 初始函数只会在加载时调用一次。这个函数不会在每次重新渲染时再被调用。因此“默认状态计算”这条日志只会出现一次,除非组件又重载了。


上面的代码改成这样:


/**   @param {number} page    @param {string} type **/const myComponent({page, type}) {  const defaultState = () => ({     fetched: someOperationValue(),     type,   })
const [state, setState] = useState(defaultState);
// if you really need to update state based on prop change, // do so here // pseudo code - if(previousProp !== prop){setState(newStateValue)}
return <ExpensiveComponent /> }
复制代码


下面来谈一些更隐蔽的场景。

把 useMemo 当作 ESLint Hook 警告的救命稻草


看看这些评论就能知道,人们在想方设法避免官方的ESLint Hooks插件发出 lint 警告。我也很理解他们的困境。


我同意Dan Abramov的观点。遏制插件中的 eslint-warnings 可能会在将来某天付出相应的代价。


一般来说,我认为我们不应该在生产环境的应用程序中遏制这些警告,这样做的话将来就更有可能出现一些隐蔽的错误。


话虽如此,有些情况下我们还是想要遏制这些 lint 警告。以下是我遇到的一个例子。这里的代码是简化过的,方便理解:


function Example ({ impressionTracker, propA, propB, propC }) {  useEffect(() => {    // 追踪初始展示    impressionTracker(propA, propB, propC)  }, [])
return <BeautifulComponent propA={propA} propB={propB} propC={propC} /> }
复制代码


这是一个相当棘手的问题。


在上面这个场景中你不关心 props 是否改变。你只想用随便哪个初始 props 调用 track 函数。这就是展示跟踪(impression tracking)的工作机制。你只能在组件加载时调用展示跟踪函数。这里的区别是你需要使用一些初始 props 调用该函数。


你可能会想只要简单地将 props 重命名为 initialProps 之类的东西就能解决问题了,但这是行不通的。这是因为 BeautifulComponent 也需要接收更新的 prop 值。



在这个示例中,你将收到 lint 警告消息:“React Hook useEffect 缺少依赖项:‘impressionTracker’、‘propA’、‘propB’和’propC’。可以包含它们或删除依赖数组。“


这条消息语气很让人不爽,但 linter 也只是在做自己的工作而已。简单的解决方案是使用 eslint-disable 注释,但这种方法不见得是最合适的,因为将来你可能在同一个 useEffect 调用中引入错误。


useEffect(() => {  impressionTracker(propA, propB, propC)  // eslint-disable-next-line react-hooks/exhaustive-deps}, [])
复制代码


我的建议是使用 useRef Hook 来保持对不需要更新的初始 prop 值的引用。


function Example({impressionTracker, propA, propB, propC}) {  // 保持对初始值的引用       const initialTrackingValues = useRef({      tracker: impressionTracker,       params: {        propA,         propB,         propC,     }  })
// 展示跟踪 useEffect(() => { const { tracker, params } = initialTrackingValues.current; tracker(params) }, []) // 对tracker或params没有ESLint警告
return <BeautifulComponent propA={propA} propB={propB} propC={propC} /> }
复制代码


根据我的测试,在这些情况下 linter 只会考虑 useRef。使用 useRef 后,linter 就明白引用的值不会改变,因此你不会收到任何警告!哪怕你用 useMemo 也逃不开这些警告的


例如:


function Example({impressionTracker, propA, propB, propC}) {
// useMemo记住这个值,使它保持不变 const initialTrackingValues = useMemo({ tracker: impressionTracker, params: { propA, propB, propC, } }, []) // 这里出现lint警告
// 展示跟踪 useEffect(() => { const { tracker, params} = initialTrackingValues tracker(params) }, [tracker, params]) // 这些依赖项必须放在这里
return <BeautifulComponent propA={propA} propB={propB} propC={propC} />}
复制代码


上面这个方案就是错误的,即使我用 useMemo 记忆初始 prop 值来跟踪初始值,最后还是无济于事。在 useEffect 调用中,记忆值 tracker 和 params 仍然必须作为数组依赖项输入。


有些人就会这样用 useMemo,这种用法不对,应该避免。我们应该使用 useRef Hook,如前所述。


总而言之,如果你真的想要消除 lint 警告的话,你会发现 useRef 是你的好朋友。

useMemo 只用于引用相等

很多人都喜欢使用 useMemo 来处理开销较大的计算并保持引用相等。我同意第一条但不同意第二条。useMemo Hook 不应该只用于引用相等。只有一种情况下可以这样做,稍后会提到。


为什么 useMemo 只用于引用相等是不对的呢?人们不都是这么做的吗?


参考下面的示例:


function Bla() {  const baz = useMemo(() => [1, 2, 3], [])  return <Foo baz={baz} />}
复制代码


在组件 Bla 中,baz 值之所以被记忆不是因为对数组[1,2,3]的评估开销很大,而是因为对 baz 变量的引用在每次重新渲染时都会改变。


虽然这看起来不是个问题,但我认为这里不应该使用 useMemo 这个 Hook。


首先,我们看看数组依赖。


useMemo(() => [1, 2, 3], [])
复制代码


这里,一个空数组被传递给 useMemo Hook。也就是说值[1,2,3]仅计算一次——也就是组件加载的时候。


因此我们得出:被记忆的值计算开销并不大,并且在加载之后不会重新计算。


出现这种情况时,希望你能重新考虑要不要用 useMemo Hook。你正在记忆一个不是计算开销并不大的值,它将来也不会重新计算。这不符合“memoization”一词的定义。


这个 useMemo Hook 的用法大错特错。它在语义上就错了,而且会消耗更多内存和计算资源。


那你该怎么办?


首先,作者在这里究竟想要做什么?他们不是要记住一个值;相反,他们希望在重新渲染时保持对值的引用不变。


别让那条黏糊糊的变色龙钻了空子。在这种情况下请使用 useRef Hook。


例如,如果你真的讨厌使用当前属性(就像我的很多同事一样),那么只需解构并重命名即可,如下所示:


function Bla() {  const { current: baz } = useRef([1, 2, 3])  return <Foo baz={baz} />}
复制代码


问题解决了。


实际上,你可以使用 useRef 来保持对开销较大的函数评估的引用——只要该函数不需要在 props 更改时重新计算就没问题。


在这些情况下 useRef 才是正确的 Hook,useMemo Hook 不合适。


使用 useRef Hook 来模仿实例变量是 Hook 的强大武库中用的最少的武器之一。useRef Hook 能做的事情远不止保持对 DOM 节点的引用。尽情拥抱它吧。


请记住这里的条件,不要只为了保持一致的引用就记忆一个值。如果你需要根据更改的 prop 或值重新计算该值,那就请随意使用 useMemo Hook。在某些情况下你仍然可以使用 useRef——但是给定数组依赖列表时 useMemo 最方便。

总结

远离狮子,也不要让变色龙钻了你的空子。如果你放进来变色龙,它们就会改变自己的肤色,融入你的代码库,影响你的代码质量。别给它们机会。


英文原文:https://blog.logrocket.com/rethinking-hooks-memoization/?from=singlemessage&isappinstalled=0


2019-08-22 08:1013474

评论

发布
暂无评论
发现更多内容

报名倒计时 | 超硬核!第四届中国云计算基础架构开发者大会邀你参会

OpenAnolis小助手

云原生 深圳 龙蜥社区 机密计算 CID

[支持M1兼容14系统]axure rp 10 mac中文版 Axure RP 10授权激活图文教程

晴雯哥

深入理解MySQL锁机制原理

这我可不懂

MySQL mysql锁原理

深度学习CV任务的突破与优化

百度开发者中心

人工智能 深度学习 大模型训练

NFTScan 支持非 EVM 公链的 NFT Collection 的认证功能

NFT Research

NFT NFT\ NFTScan

双翻页大屏看书,Mate X5上的华为阅读让你“阅”如纸上

最新动态

低代码如何赋能实体经济走向数实融合

力软低代码开发平台

Mac移植版 Nebula街机模拟器最新汉化包

胖墩儿不胖y

mac游戏 游戏推荐

大模型训练的轻量化视觉预训练模型

百度开发者中心

人工智能 大模型训练

私有化部署助力企业信息安全,WorkPlus助您完美替代企微、钉钉、飞书!

WorkPlus

Camtasia 2023 for Mac(视频录制和剪辑软件) v2023.3.1中文激活版

mac

苹果mac Windows软件 Camtasia 2023 视频软件

选择美国高防服务器,保障您的业务不受网络攻击

一只扑棱蛾子

美国高防服务器 高防服务器

数字孪生智慧市政三Web3D可视化管理平台

2D3D前端可视化开发

物联网 可视化 智慧城市 数字孪生 智慧市政

华新丽华∣国产化价值替代的先行者

用友BIP

用友 Fast by BIP

优化模型之”标注错误“

矩视智能

深度学习 机器视觉

重构AI智慧未来,小度全屋智能生态再进化

新消费日报

DR5白金版 for mac(PS一键磨皮插件Delicious Retouch)支持ps2022 v5.0汉化版

晴雯哥

2023年知名国产数据库厂家汇总

行云管家

数据库 国产化 数据运维 数据安全运维 信创国产化

大模型训练:Transformer模型、架构与训练方法

百度开发者中心

人工智能 大模型训练

HarmonyOS语言基础类库开发指南上线啦!

HarmonyOS开发者

HarmonyOS

内部即时通讯软件,为企业协同办公保驾护航

WorkPlus

九月 Web3 游戏报告: 数量增长,巨头入场,用户获取和留存仍存挑战

Footprint Analytics

区块链游戏 NFT Web3 游戏 Web3 Games

通过 Random 和 UUID 算法实现 JMeter 的随机数生成

Liam

程序员 测试 Jmeter 测试工具 随机数

技术同学如何设计职业规划

老张

职业规划 职场成长 职场发展

开源贡献难吗?

字节跳动云原生计算

flink 开源 字节

七个开发者不可不知的VS Code小技巧

树上有只程序猿

vscode

OpenHarmony应用全局的UI状态存储:AppStorage

OpenHarmony开发者

OpenHarmony

全球领先的即时通讯厂家,为企业提供卓越沟通解决方案

WorkPlus

数据库安全运维是什么意思?数据库安全运维系统用哪家好?

行云管家

数据库 数据安全 数据库安全 数据安全运维

MySQL的自增id会用完吗?用完怎么办

互联网工科生

MySQL MySQL自增ID

Snagit for mac(屏幕截图工具) 2023.2.4永久激活版

mac

苹果mac Windows软件 屏幕截图软件 Snagit 2023

不要再滥用useMemo了!你应该重新思考Hooks memoization_语言 & 开发_Ohans Emmanuel_InfoQ精选文章