一文讲透useMemo和useCallback

在React项目中是经常会使用到useMemo,useCallBack的,这是两个优化性能的方法,那么useMemo,useCallBack到底是什么呢?什么时候用呢? 下面将给打击分享相关知识,希望对大家有所帮助同时欢迎讨论指出问题!

1.useMemo
  • useMemo:会在在组件首次加载和重渲染期间执行,执行的函数需要和渲染相关的。

使用示例如下:

import React, {useMemo, useState} from "react";

function Memo (){

const [count, setCount] = useState(0)

const [bool, setBool] = useState(true)

useMemo(() => { console.log('useMemo') }, [count])

return ( <div>

<div>{count}</div>

<div>{bool ? '正面' : '反面'}</div>

<button onClick={() => setCount(count + 1)}>+1哟!</button> <button onClick={() => setBool(!bool)}>点击取反操作</button> </div> ) } export default Memo;

useMemo 类似于Vue的计算属性computed,监听某个值的变化,根据变化的值重新计算新值;通过点击触发改变count的值,组件会重新渲染且useMemo会监听count值的变化进行重新计算,但是当我触发改变bool的值,组件只进行了重新渲染但是useMemo没有重新计算并执行,因为当前useMomo监听的时count的变化,其他变化则不会去管;

2.useCallback
  • useCallback:会在渲染期间执行,返回一个函数,useCallback是用来帮忙缓存函数的,当依赖项没有发生变化时,返回缓存的指针,当在依赖项变化的时候会更新,返回一个新的函数

使用示例:

import React, { useState, useCallback } from 'react'; export default function App() { const [count1, setCount1] = useState(0); const [count2, setCount2] = useState(0); const handleClickButton1 = () => {setCount1(count1 + 1)}; const handleClickButton2 = useCallback(() => { console.log('useCallback') setCount2(count2 + 1); }, [count2]); return ( <div> <div> <Button onClick={handleClickButton1}>点击Button1</Button> </div> <div> <Button onClick={handleClickButton2}>点击Button2</Button> </div> </div> ); }

详细讲解:经过useCallback优化后,当Button2 是点击触发时自身时才会更新,Button1只要父组件更新后才会变更。

为什么使用 useMemo 和 useCallback

使用 memo 通常有三个原因:

  1. ✅ 防止不必要的 effect。
  2. ❗️防止不必要的 re-render。
  3. ❗️防止不必要的重复计算。

后两种优化往往被误用,导致出现大量的无效优化或冗余优化。下面详细介绍这三个优化方式。

防止不必要的 effect

如果一个值被 useEffect 依赖,那它可能需要被缓存,这样可以避免重复执行 effect。

const Component = () => { // 在 re-renders 之间缓存 a 的引用 const a = useMemo(() => ({ test: 1 }), []); useEffect(() => { // 只有当 a 的值变化时,这里才会被触发 doSomething(); }, [a]); // the rest of the code };

useCallback 同理:

const Component = () => { // 在 re-renders 之间缓存 fetch 函数 const fetch = useCallback(() => { console.log('fetch some data here'); }, []); useEffect(() => { // 仅fetch函数的值被改变时,这里才会被触发 fetch(); }, [fetch]); // the rest of the code };

当变量直接或者通过依赖链成为 useEffect 的依赖项时,那它可能需要被缓存。这是 useMemo 和 useCallback 最基本的用法。

防止不必要的 re-render

进入重点环节了🔔。正确的阻止 re-render 需要我们明确三个问题:

  1. 组件什么时候会 re-render。
  2. 如何防止子组件 re-render。
  3. 如何判断子组件需要缓存。
1. 组件什么时候会 re-render

三种情况:

  1. 当本身的 props 或 state 改变时。
  2. Context value 改变时,使用该值的组件会 re-render。
  3. 当父组件重新渲染时,它所有的子组件都会 re-render,形成一条 re-render 链。

第三个 re-render 时机经常被开发者忽视,导致代码中存在大量的无效缓存。

例如:

const App = () => { const [state, setState] = useState(1); const onClick = useCallback(() => { console.log('Do something on click'); }, []); return ( // 无论 onClick 是否被缓存,Page 都会 re-render <Page onClick={onClick} /> ); };

当使用 setState 改变 state 时,App 会 re-render,作为子组件的 Page 也会跟着 re-render。这里 useCallback 是完全无效的,它并不能阻止 Page 的 re-render。

2. 如何防止子组件 re-render

必须同时缓存 onClick 和组件本身,才能实现 Page 不触发 re-render。

const PageMemoized = React.memo(Page); const App = () => { const [state, setState] = useState(1); const onClick = useCallback(() => { console.log('Do something on click'); }, []); return ( // Page 和 onClick 同时 memorize <PageMemoized onClick={onClick} /> ); };

由于使用了React.memo,PageMemoized 会浅比较 props 的变化后再决定是否 re-render。onClick 被缓存后不会再变化,所以 PageMemoized 不再 re-render。

然而,如果 PageMemoized 再添加一个未被缓存的 props,一切就前功尽弃 🤯 :

const PageMemoized = React.memo(Page); const App = () => { const [state, setState] = useState(1); const onClick = useCallback(() => { console.log('Do something on click'); }, []); return ( // page WILL re-render because value is not memoized <PageMemoized onClick={onClick} value={[1, 2, 3]} /> ); };

由于 value 会随着 App 的 re-render 重新定义,引用值发生变化,导致 PageMemoized 仍然会触发 re-render。

现在可以得出结论了,必须同时满足以下两个条件,子组件才不会 re-render:

  1. 子组件自身被缓存。
  2. 子组件所有的 prop 都被缓存。
3. 如何判断子组件需要缓存

我们已经了解,为了防止子组件 re-render,需要以下成本:

  1. 开发者工作量的增加: 一旦使用缓存,就必须保证组件本身以及所有 props 都缓存,后续添加的所有 props 都要缓存。
  2. 代码复杂度和可读性的变化:代码中出现大量缓存函数,这会增加代码复杂度,并降低易读性。

除此之外还有另外一个成本:性能成本。 组件的缓存是在初始化时进行,虽然每个组件缓存的性能耗费很低,通常不足1ms,但大型程序里成百上千的组件如果同时初始化缓存,成本可能会变得很可观。

所以局部使用 memo,比全局使用显的更优雅、性能更好,坏处是需要开发者主动去判断是否需要缓存该子组件。

🤨 那应该什么时候缓存组件,怎么判断一个组件的渲染是昂贵的?

很遗憾,似乎没有一个简单&无侵入&自动的衡量方式。通常来说有两个方式:

人肉判断,开发或者测试人员在研发过程中感知到渲染性能问题,并进行判断。通过工具,目前有一些工具协助开发者在查看组件性能:如 React Dev Tools Profiler,这篇文章介绍了使用方式如这个 hooks:useRenderTimes另外,React 在 16.5版本后提供了 Profiler API:它可以识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分。所以可以通过 puppeteer 或 cypress 在自动化集成中测试组件性能,这很适合核心组件的性能测试。

防止不必要的重复计算

如 React 文档所说,useMemo 的基本作用是,避免在每次渲染时都进行高开销的计算。

🤨 那什么是"高开销的计算"?

高开销的计算其实极少出现,如下示例,对包含 250 个 item 的数组 countries 进行排序、渲染,并计算耗时。

const List = ({ countries }) => { const before = performance.now(); const sortedCountries = orderBy(countries, 'name', sort); // this is the number we're after const after = performance.now() - before; return ( // same ) };

结果如图所示,排序耗时仅用了 4 毫秒,而渲染图中的 List 组件(仅仅只是 button + 文字)却用了 20 毫秒,5倍的差距,代码详见 codesandbox.。 大部分情况下,我们的计算量要比这个 250 个 item 的数组少,而组件渲染要比这个 List 组件复杂的多,所以真实程序中,计算和渲染的性能差距会更大。

可见,组件渲染才是性能的瓶颈,应该把 useMemo 用在程序里渲染昂贵的组件上,而不是数值计算上。当然,除非这个计算真的很昂贵,比如阶乘计算。

至于为什么不给所有的组件都使用 useMemo,上文已经解释了。useMemo 是有成本的,它会增加整体程序初始化的耗时,并不适合全局全面使用,它更适合做局部的优化。

为什么 React 没有把缓存组件作为默认配置?

关于这点 Dan Abramov 在推文上也给出了解释(虽然是个类比 😅):评论区里 react 的另一位核心开发者 Christopher Chedeau 也参与了讨论。 简而言之,他们认为:

  1. 缓存是有成本的,小的成本可能会累加过高。
  2. 默认缓存无法保证足够的正确性。

原因 2 的原文:correctness is not guaranteed for everything because people can mutate things. Christopher Chedeau 未给出进一步解释。或许他是指可能会导致跟 PureComponent相同的问题,即浅比较 mutate things 时,由于浅比较相等,导致组件未能 update 的问题。

结论

讲到这里我们可以总结出 useMemo/useCallback 使用准则了:

  1. 大部分的 useMemo 和 useCallback 都应该移除,他们可能没有带来任何性能上的优化,反而增加了程序首次渲染的负担,并增加程序的复杂性。
  2. 使用 useMemo 和 useCallback 优化子组件 re-render 时,必须同时满足以下条件才有效 。
    1. 子组件已通过 React.memo 或 useMemo 被缓存
    2. 子组件所有的 prop 都被缓存
  3. 不推荐默认给所有组件都使用缓存,大量组件初始化时被缓存,可能导致过多的内存消耗,并影响程序初始化渲染的速度。

关于第三点有相反观点,详见:Why We Memo All the Things,作者推荐默认给全部组件都加上 React.memo,并给所有 props 都套上 useMemo。他认为这样可以降低工程师心智负担,让工程师不必再自己判断什么时候使用 memorize。

总结

相同点:

useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。

不同点:

useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件。

相关推荐
卸任9 小时前
Vibe coding工具使用心得
前端·ai编程
brien910 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance10 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M10 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写10 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李10 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易10 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding11 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马11 小时前
前沿视界
前端
kyriewen11 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员