【React Hooks原理 - useCallback、useMemo】

news2024/12/29 9:08:36

介绍

在实际项目中,useCallback、useMemo这两个Hooks想必会很常见,可能我们会处于性能考虑避免组件重复刷新而使用类似useCallback、useMemo来进行缓存。接下来我们会从源码和使用的角度来聊聊这两个hooks。【源码地址】

为什么要有这两个Hooks

在开始介绍之前我们先来了解下为什么有这两个hooks,其解决了什么问题?借用官网案例:

function ProductPage({ productId, referrer, theme }) {
  // 每当 theme 改变时,都会生成一个不同的函数
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* 这将导致 ShippingForm props 永远都不会是相同的,并且每次它都会重新渲染 */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

每当切换主题theme,ProductPage就会重新渲染,而即使ShippingForm使用memo包裹并且没有做任何更改也会重新渲染,这就是常说的父组件渲染导致子组件跟着渲染。
再看另一种情况:

function createOptions() {
    return {
      serverUrl: 'https://localhost:1234',
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions();
    const connection = createConnection();
    connection.connect();
    }, [createOptions])

在useEffect中添加了createOptions作为依赖,但是createOptions函数每次执行都返回的不同函数导致useEffect会重新执行

所以为了解决类似上面两种问题,利用缓存封装了useCallback、useMemo等hooks。

useCallback

function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;

让我们带着上面这两个问题来了解useCallback。用白话来说useCallback就是接收一个callback和依赖deps,只要依赖的deps没有改变,通过useCallback返回的函数就是同一个,以此来避免重复刷新。如果deps改变则useCallback会返回新的callback并将其缓存,以便下次对比。

从源码来看几乎所有的Hooks都被拆分为了mount、upadte两种(useContext除外),React内部会根据当前渲染阶段来判断调用那个来处理callback

// 首次挂载时
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
};

// 渲染更新时
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
};

以下会以useCallback为例,从源码上一步一步了解。

调用流程

从上面流程图能看出,当我们在组件内使用useCallback的时候,React会通过dispatcher根据渲染状态来进行不同的处理。

export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  return useCallbackImpl(callback, deps);
}

function useCallbackImpl<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

这里的dispatcher 是一个对象,它会在不同的渲染阶段指向不同的实现。在初次渲染时,它会指向 HooksDispatcherOnMount,在更新时,它会指向 HooksDispatcherOnUpdate。

mountCallback

当首次渲染时,会执行mountCallbac返回新的callback并将其和所依赖的deps缓存到memoizedState中

function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

在首次渲染时候主要做了下列事情:

  • mountWorkInProgressHook: 会创建一个hook并绑定到当前渲染的fiber中
  • 获取依赖deps,并将callback和deps缓存到当前fiber的hook中

在Function Component中,每个fiber节点都有一个自己的副作用hook list,在协调器(Reconciler)的fiber构造的beginWork阶段会将当然fiber节点的hook保存在hook list中,详情可查看这篇文章:【React架构 - Fiber构造循环】

updateCallback

更新渲染时,会执行updateCallback函数,会根据依赖是否变化来判断是否使用缓存

function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      return prevState[0];
    }
  }
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

updateCallback主要做了下列事情:

  • 通过updateWorkInProgressHook获取当前fiber节点对应的hook,并通过hook.memoizedState获取缓存的callback和deps
  • 当依赖存在时,通过areHookInputsEqual判断deps是否变化,如果没变则返回缓存中的callback,即prevState[0],否则缓存新的callback和deps,然后返回新的callback

在areHookInputsEqual中主要是通过Object.is来判断deps是否变化

function areHookInputsEqual(nextDeps, prevDeps) {
  if (prevDeps === null) {
    return false;
  }

  // 简单的长度检查
  if (nextDeps.length !== prevDeps.length) {
    return false;
  }

  // 逐一比较每一个依赖项
  for (let i = 0; i < nextDeps.length; i++) {
    if (Object.is(nextDeps[i], prevDeps[i])) {
      continue;
    }
    return false;
  }
  return true;
}

Object.is() 与 == 运算符并不等价。== 运算符在测试相等性之前,会对两个操作数进行类型转换(如果它们不是相同的类型),这可能会导致一些非预期的行为,例如 “” == false 的结果是 true,但是 Object.is() 不会对其操作数进行类型转换。
Object.is() 也不等价于 === 运算符。Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。=== 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。详细查看MDN

useMemo

useCallback、useMemo都是处于性能考虑通过缓存来避免重复执行的hook,同useCallback一样,useMemo也接收两个参数callback、deps。其区别主要是:useCallback是缓存以及返回函数,并不会调用函数,而useMemo会执行函数,缓存并换回函数的执行结果
同其他hooks一样,useMemo也分为了mount和update两个,下面一一介绍。

mountMemo

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 创建一个添加到Fiber节点上的Hooks链表
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 计算需要memo的值
  const nextValue = nextCreate();
  // hook数据对象上存的值
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

初次渲染:

  • mountWorkInProgressHook: 会创建一个hook链表并绑定到当前渲染的fiber中
  • 执行传入的callback,并将其保存到memoizedState中

updateMemo

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 找到该useMemo对应的hook数据对象
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  // 之前存的[nextValue, nextDeps]
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 判断依赖是否相等
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 相等就返回上次的值
        return prevState[0];
      }
    }
  }
  // 不相等重新计算
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

更新渲染:

  • 通过updateWorkInProgressHook获取当渲染fiber的hook链表
  • 根据areHookInputsEqual判断传入的依赖deps是否变化,如果变化则返回新的结果并缓存,否则使用缓存

总结

总的来说useMemo和useCallback相对来说源码比较简单,大致就是在首次渲染时,调用mountHook将callback/结果缓存到当前fiber节点的hoos链表(通过mountWorkInProgressHook创建)的memoizedState属性中,然后在更新渲染中获取当前fiber节点的hook信息(通过updateWorkInProgressHook获取),通过areHookInputsEqual判断是否使用缓存。

函数调用流程如下:
在这里插入图片描述
虽然useCallback、useMemo利用缓存避免了重复渲染,有利于性能优化,但是在实际项目中并不是所有的函数都需要用其包裹,大多情况下是没有意义的。主要场景就是上面提到的子组件更新和作为其他函数的依赖时:

  • 将其作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。缓存允许组件仅在依赖项更改时重新渲染。
  • 传递的函数可能作为某些 Hook 的依赖。比如,另一个包裹在 useCallback 中的函数依赖于它,或者依赖于 useEffect 中的函数。

当然如果能接受所有函数都被其包裹导致的代码可读性问题,这样记忆化处理也不会有什么问题。

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

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

相关文章

2.4章节python中字符串类型

在Python中&#xff0c;字符串&#xff08;String&#xff09;是一种基本的数据类型&#xff0c;用于表示文本信息。字符串可以包含字母、数字、标点符号或任何Unicode字符。Python中的字符串是不可变的&#xff0c;这意味着一旦创建了字符串&#xff0c;就不能更改字符串中的字…

无人机运营合格证及无人机驾驶员合格证(AOPA)技术详解

无人机运营合格证及无人机驾驶员合格证&#xff08;AOPA&#xff09;技术详解如下&#xff1a; 一、无人机运营合格证 无人机运营合格证是无人机运营企业或个人必须获得的证书&#xff0c;以确保无人机在运营过程中符合相关法规和标准。对于无人机运营合格证的具体要求和申请…

TF-IDF计算过程一步步推导详解含代码演示

相关概念 TF-IDF TF-IDF&#xff08;Term Frequency–Inverse Document Frequency&#xff09;是一种用于资讯检索与文本挖掘的常用加权技术。TF-IDF是一种统计方法&#xff0c;用以评估一个字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在…

交叉测试的优点和缺点!

交叉测试在软件测试中具有重要的地位和作用。通过交叉测试&#xff0c;可以提高软件质量、提升用户体验、增加测试覆盖率、提高测试效率以及满足市场需求和竞争优势。因此&#xff0c;在软件开发和测试过程中&#xff0c;应充分重视交叉测试的实施和应用。 以下是对其优缺点的…

STM32/GD32驱动步进电机芯片TM2160

文章目录 官方概要简单介绍整体架构流程 官方概要 TMC2160是一款带SPI接口的大功率步进电机驱动IC。它具有业界最先进的步进电机驱动器&#xff0c;具有简单的步进/方向接口。采用外部晶体管&#xff0c;可实现高动态、高转矩驱动。基于TRINAMICs先进的spreadCycle和stealthCh…

el-input-number 点击加减只能加一次

el-input-number 点击加减只能加一次 <el-input-number v-model"editForm.quantity" placeholder"请输入下单数量(店均)" change"quantityChangeFn"></el-input-number>需要在方法里面加 this.$forceUpdate() quantityChangeFn(val…

Postman保存API返回的token以全局使用的整个流程

1、 调通获取token的接口&#xff0c;包含传递参数的类型&#xff0c;和输入密码是否需要md5加密&#xff0c;根据接口的要求&#xff0c;传入数据 2、 查看接口响应的报文&#xff0c;可以看到token的有效时间&#xff0c;token的类型&#xff0c;里面的access_token就是想要获…

如何利用代理IP打造热门文章

作为内容创作者&#xff0c;我们都知道&#xff0c;有时候地理限制和访问障碍可能会成为我们获取新鲜素材和优质信息的障碍。使用代理IP&#xff0c;正是突破这些限制的好方法&#xff01; 1. 无缝获取全球视野 如果你还在苦恼看不到其他地区的热点文章&#xff0c;你可以尝试…

react 自定义 年-月-日 组件,单独选择年、月、日,并且产生联动

自定义 年-月-日 组件 code import { useState } from react function Year_Month_Date() {const [yearList, setYearList] useState([])const [monthList, setMonthList] useState([])const [dateList, setDateList] useState([])const [currentYear, setCurrentYear] u…

【C++】 解决 C++ 语言报错:Invalid Cast

文章目录 引言 无效类型转换&#xff08;Invalid Cast&#xff09;是 C 编程中常见且严重的错误之一。当程序试图进行不合法或不安全的类型转换时&#xff0c;就会发生无效类型转换错误。这种错误不仅会导致程序崩溃&#xff0c;还可能引发不可预测的行为。本文将深入探讨无效…

PLL和CDR的内部结构及其区别

比较PLL和CDR的内部结构及其区别&#xff1a; 基本结构&#xff1a; PLL&#xff08;相位锁定环&#xff09;&#xff1a; 相位检测器环路滤波器压控振荡器&#xff08;VCO&#xff09;分频器&#xff08;可选&#xff0c;用于频率合成&#xff09; CDR&#xff08;时钟数据恢复…

jmeter-beanshell学习2-beanshell断言

继续写&#xff0c;之前写了获取变量&#xff0c;设置变量&#xff0c;今天先写个简单点的断言。 一般情况用响应断言&#xff0c;就挺好使&#xff0c;但是自动化还要生成报告&#xff0c;如果断言失败了&#xff0c;要保存结果&#xff0c;只能用beanshell处理&#xff0c;顺…

昇思25天学习打卡营第17天(+1)|Diffusion扩散模型

1. 学习内容复盘 本文基于Hugging Face&#xff1a;The Annotated Diffusion Model一文翻译迁移而来&#xff0c;同时参考了由浅入深了解Diffusion Model一文。 本教程在Jupyter Notebook上成功运行。如您下载本文档为Python文件&#xff0c;执行Python文件时&#xff0c;请确…

LLM - 神经网络的训练过程

1. 对于回归问题&#xff0c;用损失函数来计算预测值和真实值的差异&#xff0c;一种常用的公式是如下图所示(Mean Square Error)&#xff0c;如果损失函数的值越小说明神经网络学习越准确&#xff0c;所以神经网络训练目标是减小损失函数的值&#xff0c; 2. 对于分类问题&…

MaxKB开源知识库问答系统发布v1.3.0版本,新增强大的工作流引擎

2024年4月12日&#xff0c;1Panel开源项目组正式发布官方开源子项目——MaxKB开源知识库问答系统&#xff08;github.com/1Panel-dev/MaxKB&#xff09;。MaxKB开源项目发布后迅速获得了社区用户的认可&#xff0c;成功登顶GitHub Trending趋势榜主榜。 截至2024年7月4日&…

相关技术 太阳能热水器循环水泵制作技术

网盘 https://pan.baidu.com/s/1oAKwUEGkKnEgxE-F4znKow?pwdidxd 双温区蓄能供热型太阳能热水系统及其工作方法.pdf 双罐叠压节能恒温型太阳能热水机组.pdf 基于傅科电流的循环式风能热水器.pdf 基于太阳能利用的建筑冷热电联产系统及工作方法.pdf 基于太阳能和热泵的双蓄式热…

计算机网络之以太网

上文内容&#xff1a;总线局域网以及冲突的解决方法 1.以太网的起源 1.1起源 60年代末期&#xff0c;夏威夷大学Norman Abramson等研制ALOHA无线网络系统,实现Oahu岛上的主机和其它岛及船上的读卡机和终端通信&#xff1b; 出境信道地址&#xff1a;主机到终端&#xff1…

5款文案自动生成器,快速创作高质量文案

随着科技的发展&#xff0c;市面上出现了许多文案自动生成器&#xff0c;为我们的创作过程提供了极大的便利。无论是为了社交媒体内容创作&#xff0c;还是产品的文案的宣传&#xff0c;文案自动生成器就能为我们快速且高效地生成高质量的文案。以下将为大家分享5款备受赞誉的文…

Zabbix 配置SNMP监控

Zabbix SNMP监控介绍 Zabbix提供了强大的SNMP监控功能&#xff0c;可以用于监控网络设备、服务器和其他支持SNMP协议的设备。SNMP&#xff08;Simple Network Management Protocol&#xff0c;简单网络管理协议&#xff09;是一种广泛用于网络管理的协议。它用于监控网络设备&…

IDEA导入依赖+Maven配置

Maven安装及配置 安装 安装链接&#xff1a;https://archive.apache.org/dist/maven/maven-3/3.6.3/binaries/ 注&#xff1a;建议不要直接安装最新版本&#xff0c;选用常用、稳定的版本安装即可&#xff0c;比如&#xff1a;3.6.3 配置 1> 配置bash_profile文件 终端输…