useCallback 和 useMemo 使用场景

news/2024/11/15 13:15:37/文章来源:https://www.cnblogs.com/wpshan/p/18547770
 

一切为了性能,无论是 useCallback 还是 useMemo 还是 memo,都是为了让不该渲染的组件不去渲染

在学习 useCallback、useMemo 之前,我们需要知道一点,React 的渲染是自顶而下,如果父组件渲染了,那么子组件也会渲染,其子孙组件“世世代代”都要渲染

但如果父组件的渲染与子组件的 props 无关呢?设想一下父组件改变样式颜色,关我子组件的数据请求什么关系?

而这正是 useCallback 、useMemo、memo 的作用所在,将子组件用 memo 包住,如果 props 和上次渲染时相同,那么子组件就跳过了重新渲染,useCallback 缓存父组件传给子组件的函数,useMemo 缓存父组件传给子组件的(计算)值

如今 React19 提出 Compiler,不需要再写 useCallback 、useMemo、memo 扰乱心智了

 React Compiler

在讲 useMemo 是什么之前,我们先了解下什么是 Memo

Memo​

官方定义:React.memo 是一个高阶组件(HOC),其主要目的是优化函数组件的性能。它通过记忆组件的渲染输出,使得当组件的 props 没有变化时,可以跳过该组件的重新渲染,从而提升性能

它会进行浅层对比,即对比两个对象或数组时只检查其第一层属性的相等性方法,在此比较中,仅比较对象的直接属性值,而不递归地深入到镶嵌的对象或数组中

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = obj1;console.log(obj1 === obj2); // false,因为 obj1 和 obj2 指向不同的内存地址
console.log(obj1 === obj3); // true,因为 obj3 引用同一个 obj1

和浅拷贝一个道理,只考察第一层属性,不递归更深层的对象和数组

举个栗子🌰

jsx
...
function App() {
    const [n, setN] = useState(0)
    const [m, setM] = useState(0)
    const onClick = () => {
        setN(n + 1)
    }
    
    return (
    	<div className="App">
			<div>
            	<button onClick={onClick}>update n {n}</button>
            </div>	
            <Child data={m} />
        </div>
    )
}

function Child(props) {
    console.log("child 执行了")
    return <div>child: {props.data}</div>
}
...

当我们什么都不做时,点击 button,Child 组件会重新 re-render,Child 组件为什么会重新 re-render,因为 button 点击后,n变化,导致 App re-render,所以 App 中的 Child 组件也跟着重新渲染

但这不对,因为 Child 的依赖项是 m,m没变化,Child 不需要重新渲染才对,所以我们需要加上 React.memo

jsx
...
function App() {
    const [n, setN] = useState(0)
    const [m, setM] = useState(0)
    const onClick = () => {
        setN(n + 1)
    }
    
    return (
    	<div className="App">
			<div>
            	<button onClick={onClick}>update n {n}</button>
            </div>	
            <Child2 data={m} />
        </div>
    )
}

function Child(props) {
    console.log("child 执行了")
    return <div>child: {props.data}</div>
}

const Child2 = React.memo(Child)

如此一来,Child2 就不会重新 re-render 了

也就是说,如果 props 不变,就没必要再此执行一个函数组件了

但这个做法有个 bug,加上监听函数后,一秒破功

jsx
...
function App() {
    const [n, setN] = useState(0)
    const [m, setM] = useState(0)
    const onClick = () => {
        setN(n + 1)
    }
    
    const onChildClick = () => {
        console.log('点击 child')
    }
    
    return (
    	<div className="App">
			<div>
            	<button onClick={onClick}>update n {n}</button>
            </div>	
            <Child2 onClick={onChildClick} data={m} />
        </div>
    )
}

function Child(props) {
    console.log("child 执行了")
    return <div onClick={props.onClick}>child: {props.data}</div>
}

const Child2 = React.memo(Child)

当加上函数后,Child2 就会重新 re-render,理由也很简单

  • button 点击后,触发 App re-render
  • 生成的 onChildClick 是一个新的函数
  • 函数属于对象,对象都是引用类型,所以心的 onChildClick 和之前的 onChildClick 不是同一个引用
  • 故 Child 即使用 memo 包裹了,但是会因为 props(onClick)变化而重新渲染

这时就要用到 useMemo

jsx
 
...
const onChildClick = useMemo(() => {
    return () => {
        console.log('点击 child')
    }
}, [m])
 ...

如此就缓存住了 onChildClick

useMemo​

useMemo 是一个 React Hook,它在每次重新渲染时都能缓存计算的结果

jsx
const cachedValue = useMemo(calculateValue, dependencies)

第一个参数是 ()=> value

第二个参数是依赖项 [m,n]

只有当依赖项变化时,才会计算出新的 value

如果依赖项不变,那就用之前的 value

如果你的 value 是个函数,那么你就要写成 useMemo(() => x => console.log(x))

使用场景​

跳过组件的重新渲染​

某些情况下,useMmeo 可以帮助你优化重新渲染子组件的性能。假设这个 TodoList 组件将 visibleTodos 作为 props 传递给子 List 组件:

jsx
export default function TodoList({ todos, tab, theme }) {
  const visibleTodos = filterTodos(todos, tab);
  return (
    <div className={theme}>
      <List items={visibleTodos} />
    </div>
  );
}

将 List 用 memo 包裹住后,保持 props 不变就能实现 List 不渲染

所以我们使用 useMemo 缓存 filterTodos 的值

jsx
export default function TodoList({ todos, tab, theme }) {
  // 告诉 React 在重新渲染之间缓存你的计算结果...
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab] // ...所以只要这些依赖项不变...
  );
  return (
    <div className={theme}>
      {/* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */}
      <List items={visibleTodos} />
    </div>
  );
}

通过将 visibleTodos 的计算函数包裹在 useMemo 中,你就可以确保它在重新渲染之间具有相同值,直到依赖项发生变化。

如果 useMemo 是个值还好说,如果是返回函数的函数,如useMmeo(()=>(x) => console.log(x)) 不仅难用,而且难以理解,于是 React 团队就写了语法糖——useCallback

最开始的案例我们修改下:

jsx
 
...
const onChildClick = useCallback(() => {
    console.log('点击 child')
}, [m])
 ...

它的作用和 useMemo 一模一样,只是针对的是函数

useCallback​

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook

jsx
const cachedFn = useCallback(fn, dependencies)

用法​

它和 useMemo 出自一脉

useCallback( x => log(x), [m]) 等价于 useMemo(() => x => log(x), [m])

跳过组件的重新渲染​

当你优化渲染性能时,有时需要缓存传递给子组件的函数。为了缓存组件中多次渲染的函数,你需要将其定义在 useCallback Hook 里:

jsx
import { useCallback } from 'react';

function ProductPage({productId, referer, theme}) {
    const handleSubmit = useCallback((orderDetails) => {
        post('/product/' + productId + '/buy', {
            referer,
            orderDetails
        })
    }, [productId, referer])
}

你需要传递两个参数给 useCallback:

  1. 在多次渲染中需要缓存的函数
  2. 函数内部需要使用的所有组件内部值的依赖值

初次渲染时,在 useCallback 处接受的 返回函数 将会是已经传入的函数

在之后的渲染中,React 将会使用 Object.is 把当前的依赖和已传入之前的依赖进行比较。如果没有任何依赖改变,useCallback 在多次渲染中缓存一个函数,直到这个函数的依赖发生变化

简而言之,useCallback 在多次渲染中缓存一个函数,直至这个函数的变化

useCallback 与 useMemo 有何关系?​

useCallback 经常和 useMemo 一同出现。当尝试优化子组件时,它们都很有用。因为它们会记住(或者说:缓存)正在传递的东西:

jsx
import { useMemo, useCallback } from 'react';

function ProductPage({ productId, referrer }) {
  const product = useData('/product/' + productId);

  const requirements = useMemo(() => { //调用函数并缓存结果
    return computeRequirements(product);
  }, [product]);

  const handleSubmit = useCallback((orderDetails) => { // 缓存函数本身
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

  return (
    <div className={theme}>
      <ShippingForm requirements={requirements} onSubmit={handleSubmit} />
    </div>
  );
}

区别在于你需要缓存什么:

  • useMemo 缓存函数调用的结果。在这里,它缓存了调用 computeRequirements(product) 的结果。
  • useCallback 缓存函数本身。不像 useMemo,它不会调用你传入的函数。相反,它缓存此函数。

如果你已经熟悉了 useMemo,你会发现 useCallback 就是 useMemo 所实现:

js
// 在 React 内部的简化实现
function useCallback(fn, dependencies) {
  return useMemo(() => fn, dependencies);
}

是否应该在任何地方添加 useCallback?​

大部分业务代码不需要使用缓存,但如果你的应用更像是一个绘制编辑器,需要用到大量交互,缓存会很有用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/833990.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[LeetCode 最大岛屿面积

### DFS解法``` python class Solution:dir = [(-1,0),(1,0),(0,-1),(0,1)]def dfs(self,grid,x,DFS解法 class Solution:dir = [(-1,0),(1,0),(0,-1),(0,1)]def dfs(self,grid,x,y):if x < 0 or x >= len(grid) or y < 0 or y >= len(grid[0]) or grid[x][y] != 1…

【Ubuntu】在Ubuntu上安装微信

【Ubuntu】在Ubuntu上安装微信 零、说明 微信官网最近发布了Linux的测试版本,实际使用下来与Windows版本相差不多,本文介绍如何在Ubuntu(Debian系)上安装Linux版本的微信。 壹、下载 打开Linux微信官网:https://linux.weixin.qq.com/,根据自己的处理器架构选择对应的deb格…

文件共享服务之NFS挂载实验

任务需求 1.部署一台web服务器,提供静态网页的展示,该网站的html等静态资源远程存储在NFS服务器。 2.部署NFS服务器,创建共享文件夹(提供静态文件),发布该共享目录,提供给web服务器使用。 主机列表 # 外网地址 内网地址 主机名 192.168.122.207…

第 5 篇 Scrum 冲刺博客

团队作业4——第 5 篇 Scrum 冲刺博客 作业要求这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13234这个作业的目标 团队集体协作完成项目开发队名 雄狮般的男人站立式…

Sitecore debug 工具

由于 Sitecore 的调试需要老是通过 log 来分析,而每次更新 dll 都需要等待一定的时间和重复执行前端的操作逻辑,特开发一个在线编辑器的方式来方便调试。警告:请勿在生产环境使用。截图构建项目 打开 Frontend,使用 pnpm i 安装依赖包,然后 pnpm build:vite 构建项目,之后…

如何禁止 SQL Server 中的 xp_cmdshell 以提高安全性

概述 在 SQL Server 中,xp_cmdshell 是一个强大的功能,它允许执行操作系统级别的命令。然而,这也带来了潜在的安全风险。本文将详细介绍如何禁止 xp_cmdshell,以增强 SQL Server 的安全性。 禁止 xp_cmdshell 的步骤 步骤 1:检查 xp_cmdshell 的当前状态 在开始禁止 xp_cm…

“亦”真“亦”假?——MXsteerWheel与DYNA4的强强联手

高性能力反馈方向盘MXsteerWheel作为线控转向开发的新帮手,在北汇信息的展台上一直是大家关注的焦点。由于它简练出众的外表与真实阻尼的手感,吸引参展的朋友都乐此不疲地进行尝试。而后又不禁感叹,亦真亦假!图1 桌面式驾驶模拟器 一、系统组成整体来看,驾驶模拟器由CANo…

c++_primer之第四章

4.1 节练习 练习 4.1 在算术运算符中,乘法和除法的优先级相同,且均高于加减法的优先级。因此 上式的计算结果应该是 105,在编程环境中很容易验证这一点。 练习 4.2 在本题涉及的运算符中,优先级最高的是成员选择运算符和函数调用运算符, 其次是解引用运算符,最后是加法运…

Docker Kubernetes

Docker镜像与容器 Docker 中有两个重要概念。 一个是容器(Container):容器特别像一个虚拟机,容器中运行着一个完整的操作系统。可以在容器中装 Nodejs,可以执行npm install,可以做一切你当前操作系统能做的事情 另一个是镜像(Image):镜像是一个文件,它是用来创建容器…

thinkphp升级后报错Declaration of think\app\Url::build() must be compatible with think\route\Url::build():

​ 将源码中的thinkphp升级后,发现了错误:Declaration of think\app\Url::build() must be compatible with think\route\Url::build(): string 出现这个错误的原因是,你通过命令“composer update topthink/framework”只升级了框架,没有更新多应用扩展模块。 只需要compo…

为什么 PHP 在 2024 年会越来越受欢迎:经典语言的意外回归

2024 年,PHP 出人意料地卷土重来,这得益于重大的性能改进、现代功能和蓬勃发展的生态系统。 在 Laravel 等框架的引领和广泛的托管支持下,PHP 已成为强大、快速且可靠的 Web 开发选择,这些证明它远未过时。 二十多年来,PHP 一直是 Web 开发的基石,为数百万个网站和 Web 应…

linux终端美化 oh-my-bash安装

之前一直在用zsh终端,然后安装oh-my-zsh,配置后感觉特别清爽,于是想bash终端下是否存在类似的软件,找了下发现了oh-my-bash。特记录下安装使用过程 oh my bash官网:https://ohmybash.nntoan.com/ oh my bash github:https://github.com/ohmybash/oh-my-bash系统终端SHELL…