React hooks之useCallback的使用与性能分析

news2024/9/20 11:02:01

使用useCallback优化代码

useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

1.原理分析

useCallback是React Hooks中的一个函数,用于优化函数组件的性能。它的作用是返回一个memoized(记忆化的)函数,这个函数只有在依赖项发生变化时才会重新计算,否则会直接返回上一次计算的结果。
useCallback是对传过来的回调函数优化,返回的是一个函数;useMemo返回值可以是任何,函数,对象等都可以。
简单来说就是返回一个函数,只有在依赖项发生变化的时候才会更新(返回一个新的函数)。

2.案例分析:

父组件定义一个请求函数fetchData,和一个状态query,将query当作fetchData的参数,将该函数传递进子组件,当父组件query发生变化时,让子组件调用该函数发起请求。

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {    
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;    // ... Fetch data and do something ...  };  render() {
    return <Child fetchData={this.fetchData} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    // 🔴 This condition will never be true
    if (this.props.fetchData !== prevProps.fetchData) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

在本代码中,fetchData是一个class方法!(或者你也可以说是class属性)它不会因为状态的改变而不同,所以this.props.fetchData和 prevProps.fetchData始终相等,因此不会重新请求。

2.1旧思维–优化该案例:

子组件使用:

componentDidUpdate(prevProps) {
    this.props.fetchData();
}

这样可以发起请求,但是会在每次渲染后都去请求。
或者改变父组件:

render() {
    return <Child fetchData={this.fetchData.bind(this, this.state.query)} />;
}

但这样一来,this.props.fetchData !== prevProps.fetchData 表达式永远是true,即使query并未改变。这会导致我们总是去请求。(bind() 方法会创建一个新的函数对象)
唯一现实可行的办法是把query本身传入 Child 组件。 Child 虽然实际并没有直接使用这个query的值,但能在它改变的时候触发一次重新请求:

class Parent extends Component {
  state = {
    query: 'react'
  };
  fetchData = () => {
    const url = 'https://hn.algolia.com/api/v1/search?query=' + this.state.query;
    // ... Fetch data and do something ...
  };
  render() {
    return <Child fetchData={this.fetchData} query={this.state.query} />;  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {      this.props.fetchData();    }  }
  render() {
    // ...
  }
}

在class组件中,函数属性本身并不是数据流的一部分。使用useCallback,函数完全可以参与到数据流中。我们可以说如果一个函数的输入改变了,这个函数就改变了。如果没有,函数也不会改变。

2.2开始使用hooks:

场景一: 使用函数组件
但父组件不使用useCallback处理函数

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = () => {
    console.log('新的fetch');
    return query;
  }
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

function Child({ fecthData }: { fecthData: any }) {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
}

export default App;

初始化的时候:
在这里插入图片描述
点击按钮:

在这里插入图片描述
但是从图里面可以看到,点击addOther时,并没有使得query发生变化,但是子组件仍然调用了该函数发起请求。可以看到这种方法需求可以使得子组件在父组件的状态query发生变化时,成功发起了请求,但是还是存在副作用。
问题的原因在于状态queryOther的改变,使得父组件重新渲染,重新生成了fecthData函数,并返回了该函数新的地址,导致子组件刷新。

场景二:父组件使用useCallback处理函数

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = useCallback(() => {
    console.log('新的fetch');
    return query;
  }, [query])
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

function Child({ fecthData }: { fecthData: any }) {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
}

export default App;

初始状态
在这里插入图片描述
点击按钮:
在这里插入图片描述

可以看到只有点击+1按钮改变query才会使得子组件发起请求,点击other+1已经没有处罚上文副作用。

原因分析:
使用了useCallback,useCallback的工作原理是什么?useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址。不论是否使用useCallBack都无法阻止组件render时函数的重新创建。在本例子中点击按钮other+1并没有使得query发生变化,所以并没有返回新的fetchData函数地址,又因为在子组件中使用useEffect对fetchData监听时,所以子组件不会发起请求。但是,点击按钮other+1时,子组件虽然没发起请求,但是还是刷新了,这是什么原因呢?这是因为子组件直接在父组件中挂载,没有做过任何优化,当父组件重新渲染时,会导致子组件也跟着渲染。所以单纯的使用useCallback可以监听到相应变化,使得子组件做出变化,但是并不能优化性能。所以当我们不用监听某个状态使得函数发生改变时,不要轻易使用useCallback,因为使用 useCallBack后每次执行到这里内部比对是否变化,还有存一下之前的函数,消耗更大了。

场景三:优化上述问题,搭配React.memo使用

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = useCallback(() => {
    console.log('新的fetch');
    return query;
  }, [query])
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>  ,,m

const Child = React.memo(({ fecthData }: { fecthData: any }) => {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
})


export default App;

初始状态:
在这里插入图片描述
点击按钮:
在这里插入图片描述
一切问题都解决了。点击other+1按钮,没有使得子组件发起请求,也没有使得子组件因为这个无关变量的变化,导致重新渲染。

原因分析:

  • 使用useCallback使得无关变量变化时,阻止了新创建的fetchData的新地址返回,传给子组件的还是原本的函数地址(useCallBack的本质工作不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址)
  • React.memo 这个方法,此方法内会对 props 做一个浅层比较,如果如果 props 没有发生改变(useCallback的存在使得props没变化),则不会重新渲染此组件。

场景四:单纯使用React.memo会发生什么

import React, { useCallback, useState, useEffect } from 'react';
import './App.css';

function App() {
  const [query, setQuery] = useState(1);
  const [queryOther, setQueryOther] = useState(1);
  const fecthData = () => {
    console.log('新的fetch');
    return query;
  }
  const add = () => {
    console.log('点击add');
    setQuery(query + 1);
  }
  const addOther = () => {
    console.log('点击addOther');
    setQueryOther(queryOther + 1);
  }
  return (
    <>
      <Child fecthData={fecthData} />
      <button onClick={add}>+1</button>
      <button onClick={addOther}>other+1</button>
      <div>{ query }</div>
    </>
  );
}

const Child = React.memo(({ fecthData }: { fecthData: any }) => {
  console.log('子组件相关内容');
  useEffect(() => {
    const querN = fecthData();
    console.log('子组件调用该函数获取到相关内容', querN);
  }, [fecthData])
  return <div>
    123
  </div>
})


export default App;

初始状态:
在这里插入图片描述
点击按钮
在这里插入图片描述
React.memo检测的是props中数据的栈地址是否改变。而父组件重新构建的时候,会重新构建父组件中的所有函数(旧函数销毁,新函数创建,等于更新了函数地址),新的函数地址传入到子组件中被props检测到栈地址更新。也就引发了子组件的重新渲染。所以,在上面的代码示例里面,子组件是要被重新渲染的。上文中的fetchData因为失去了useCallback的保护使得子组件的props发生了变化,从而React.memo也失去了作用,而且因为fetchData因为失去了useCallback的保护,使得点击other+1按钮改变无关的变量时,子组件也调用了请求函数。

3.useCallback使用总结:

  • 可以使用useCallback可以监听到相应状态变化,使得父/子组件做出响应。
  • 但是滥用useCallback会影响性能,需搭配React.memo进行使用,否则适得其反。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue项目打包后如何本都部署访问

npm run build生成dist项目后&#xff0c;在windows部署访问。 方式一&#xff1a; 1、新建一个文件夹 进入目录后打开cmd 输入npm init -y 2、输入 npm i express -s 是用于在 Node.js 项目中安装 Express 框架的命令 3、.将项目打包好的dist文件放入其中以及新建一个app.js文…

C++ 二叉搜索树

1. 内容安排说明 二叉树在前面 C 数据结构阶段已经讲过&#xff0c;本节取名二叉树进阶是因为&#xff1a; 1. map 和 set 特性需要 先铺垫二叉搜索树&#xff0c;而二叉搜索树也是一种树形结构 2. 二叉搜索树的特性了解&#xff0c;有助于更好的理解 map 和 set 的特性 …

在JDK17尝鲜Flink1.17

在JDK17尝鲜Flink1.17 前言 还没玩明白老版本&#xff0c;Flink1.17就来了&#xff01;&#xff01;&#xff01;总还是要向前看的。。。 根据官网文档&#xff1a;https://nightlies.apache.org/flink/flink-docs-release-1.17/docs/try-flink/local_installation/ Flink r…

【Excel技巧】如何将一堆文字快速整理成一列表格数据?

在平时的工作中&#xff0c;我们有时候需要把很多零散的分布的内容&#xff08;比如姓名&#xff09;&#xff0c;复制到Excel工作表的单元格内&#xff0c;变成一列。如果一个个复制粘贴&#xff0c;显然太过繁琐。 如何批量快速的完成这一操作呢&#xff1f;只需要下面简单几…

关于排查springboot启动时页面出现404

今天在进行开发时&#xff0c;Contronller代码没有问题&#xff0c;前端html也没问题&#xff0c;发现当浏览器输入localhost:8080时404&#xff0c;于是进行排查发现&#xff0c;SpringbootWebApplication文件放到了子目录下。 springboot的启动文件必须放在父目录下才可以检测…

【Git原理与使用】-- 远程操作

目录​​​​​​​ 理解分布式版本控制系统 远程仓库 新建远程仓库 lssue 与 Pull Request模板文件 知识铺垫 lssue 模板文件 Pull Request模板文件 克隆远程仓库 使用 HTTPS 方式 使用 SSH 方式 第一步&#xff1a;创建SSH Key 向远程仓库推送 过程梳理 实操 …

Java安全——安全提供者

Java安全 安全提供者 在Java中&#xff0c;安全提供者&#xff08;Security Provider&#xff09;是一种实现了特定安全服务的软件模块。它提供了一系列的加密、解密、签名、验证和随机数生成等安全功能。安全提供者基础设施在Java中的作用是为开发人员提供一种扩展和替换标准…

vue中使用Drawflow连线插件,并对端口进行命名

效果如图 场景:项目中需要拖拽模块并连线,有输入端和输出端之分,不同模块不同端口才能相连 文档相关 点击前往------->原项目git地址 点击前往------->提供端口既可输出又可输出方案 点击前往----->查阅发现原项目无法对端口命名 public文件夹下创建drawflow文件夹…

myCobot 280 2023机械臂全新功能,手柄控制、自干涉检测

引言 机械臂是一种可编程的、自动化的机械系统&#xff0c;它可以模拟人类的动作&#xff0c;完成各种任务&#xff0c;例如装配、喷涂、包装、搬运、焊接、研磨等。由于其高度灵活性和多功能性&#xff0c;机械臂在现代社会中已经得到了广泛的应用。 myCobot 280 M5Stack 20…

在服务器部署前后端分离的项目(前后都有), 并使用nginx配置跨域

怎样部署自己的项目呢 先准备一个服务器(小系统最便宜的轻量级服务器就行, 如果不需要给人访问的话)安装宝塔面板 (宝塔面板, 可视化界面, 操作简单, 使用非常方便, 上手也很容易, 如果只是学习, 虚拟机也行没必要花钱, 我使用的CentOS7系统,安装宝塔面板)软件: MySQL, Tomcat…

【问题记录】多线程环境下,使用 std::cout 输出内容会显示混乱

环境 Windows 11 家庭中文版Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.5.3 测试代码 #include <iostream> #include <Windows.h>//创建的线程数量 #define THREAD_COUNT 4DWORD WINAPI ThreadProc(LPVOID lpParam) {UNREFERENCED_P…

JS事件监听

目录 事件监听 事件监听案例 事件监听 事件&#xff1a;HTML事件是发生在HTML元素上的“事情” 按钮点击鼠标移动到元素上按下键盘按键事件监听&#xff1a;JS可以在事件被检测到时执行代码事件绑定 方法一&#xff1a;通过HTML标签中的事件属性进行绑定 <input type"…

在windows环境下安装支持CUDA的opencv-python

文章目录 附件&#xff1a;GPU和CUDA的关系 —— 开发人员通过CUDA可以使用GPU的计算能力来加速各种计算任务&#xff0c;并提高计算性能和效率。一、环境配置&#xff08;0&#xff09;我的电脑配置环境&#xff08;1&#xff09;CUDA cuDNN下载与安装&#xff08;2&#xff…

【云原生、Kubernetes】Kubernetes核心概念理解

首先我们要掌握 Kubernete 的一些核心概念。 这些核心可以帮助我们更好的理解 Kubernetes 的特性和工作机制。 集群组件 首先&#xff0c;Kubernetes 集群中包含2类节点&#xff0c;分别是&#xff1a;master控制节点和node工作节点。 master 控制节点 负责管理整个集群系统…

【手撕算法|动态规划系列No.4】leetcode91. 解码方法

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

软件测试:系统测试

1 系统测试的概念 系统测试&#xff08;System Testing&#xff09;的定义&#xff1a;将已经集成好的软件系统&#xff0c;作为整个基于计算机系统的一个元素&#xff0c;与计算机硬件、外设、某些支持软件、数据和人员等其他系统元素结合在一起&#xff0c;在实际运行&#…

HDLBits刷题笔记8:Circuits.Sequential Logic.Latches and Flip-Flops

D flip-flop module top_module (input clk,input d,output reg q );always (posedge clk)q < d; endmoduleD flip-flops 建立一个8bit的D触发器 module top_module (input clk,input [7:0] d,output reg [7:0] q );always (posedge clk)q < d; endmoduleDFF with res…

GDAL 图像直方图统计

文章目录 一、简介二、实现代码三、实现效果参考资料 一、简介 这里使用一种简单的方式来计算图像中的像素值直方图分布。计算过程如下所述&#xff1a; 第一种方式&#xff1a; 1、首先将图像变为一维数组&#xff08;reshape&#xff09;&#xff0c;并将数组中的数值进行排序…

vue点击盒子一步一步滚动

vue点击盒子一步一步滚动 HTML <div class"course_detail"><div class"arrow" v-if"index 0" click"step"></div><div class"lightArrow" v-else click"step"></div><div clas…

自定义的车牌号键盘组件

<template><view class"keyboard-wrap" v-if"kbShow"><view class"head"><view class"done" tap"done"><text class"iconfont iconxiala-"></text>关闭</view></vi…