【React Hooks原理 - useCallback、useMemo】

介绍

在实际项目中,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.rhkb.cn/news/370021.html

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

相关文章

STM32 看门狗 HAL

由时钟图可以看出看门狗采用的是内部低速时钟&#xff0c;频率为40KHz 打开看门狗&#xff0c;采用32分频&#xff0c;计数1250。 结合设置的分频系数和重载计数值&#xff0c;我们可以计算出看门狗的定时时间&#xff1a; 32*1250/40kHz 1s 主函数中喂狗就行 HAL_IWDG_Ref…

SQL使用join查询方式找出没有分类的电影id以及名称

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 现有电影信息…

第一次作业

作业1 1.代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head&…

vue require引入静态文件报错

如果是通过向后端发送请求&#xff0c;动态的获取对应的文件数据流很容易做到文件的显示和加载。现在研究&#xff0c;一些不存放在后端而直接存放在vue前端项目中的静态媒体文件如何加载。 通常情况下&#xff0c;vue项目的图片jpg&#xff0c;png等都可以直接在/ass…

Android --- 新电脑安装Android Studio 使用 Android 内置模拟器电脑直接卡死,鼠标和键盘都操作不了

新电脑安装Android Studio 使用 Android 内置模拟器电脑直接卡死&#xff0c;鼠标和键盘都操作不了 大概原因就是,初始化默认Google的安卓模拟器占用的RAM内存是2048&#xff0c;如果电脑的性能和内存一般的话就可能卡死&#xff0c;解决方案是手动修改安卓模拟器的config文件&…

大数据期末复习——hadoop、hive等基础知识

一、题型分析 1、Hadoop环境搭建 2、hadoop的三大组件 HDFS&#xff1a;NameNode&#xff0c;DataNode&#xff0c;SecondaryNameNode YARN&#xff1a;ResourceManager&#xff0c;NodeManager &#xff08;Yarn的工作原理&#xff09; MapReduce&#xff1a;Map&#xff0…

机器学习 C++ 的opencv实现SVM图像二分类的训练 (二)【附源码】

本节讲机器学习 C 的opencv实现SVM图像二分类的训练&#xff0c;下节讲测试&#xff1a; 数据集合data内容如下&#xff1a; 下载地址为&#xff1a;https://download.csdn.net/download/hgaohr1021/89506900 #include <stdio.h> #include <time.h> #include…

AIGC | 在机器学习工作站安装NVIDIA CUDA® 并行计算平台和编程模型

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] 0x02.初识与安装 CUDA 并行计算平台和编程模型 什么是 CUDA? CUDA&#xff08;Compute Unified Device Architecture&#xff09;是英伟达&#xff08;NVIDIA&#xff09;推出的并行计算平台和编…

LLMs之gpt_academic:gpt_academic的简介、安装和使用方法、案例应用之详细攻略

LLMs之gpt_academic&#xff1a;gpt_academic的简介、安装和使用方法、案例应用之详细攻略 目录 gpt_academic的简介 1、版本更新历史 版本: 1、新增功能及其描述 新界面&#xff08;修改config.py中的LAYOUT选项即可实现“左右布局”和“上下布局”的切换&#xff09; 所…

吴恩达深度学习笔记:机器学习策略(2)(ML Strategy (2)) 2.7-2.8

目录 第三门课 结构化机器学习项目&#xff08;Structuring Machine Learning Projects&#xff09;第二周&#xff1a;机器学习策略&#xff08;2&#xff09;(ML Strategy (2))2.7 迁移学习&#xff08;Transfer learning&#xff09; 第三门课 结构化机器学习项目&#xff0…

2024年加密货币市场展望:L1、L2、LSD、Web3 和 GameFi 板块的全面分析与预测

随着区块链技术的快速发展&#xff0c;加密货币市场在2024年继续展现出蓬勃的生机和创新的潜力。本文将深入分析L1、L2、LSD、Web3和GameFi这五大板块的发展趋势和预测&#xff0c;帮助投资者和爱好者更好地理解和把握市场机遇。 一、L1&#xff1a;基础层协议的持续进化 L1&a…

基于Java中的SSM框架实现物流管理系统项目【项目源码+论文说明】

基于Java中的SSM框架实现物流管理系统演示 摘要 企业的发展离不开物流的运输&#xff0c;在一个大型的企业中&#xff0c;商品的生产和建设&#xff0c;推广只是前期的一些工作&#xff0c;在后期的商品销售和物流方面的建立&#xff0c;才能让一个企业得到大力的发展。 企业…

Java经典面试题将一个字符串数组进行分组输出,每组中的字符串都由相同的字符组成

Java经典面试题将一个字符串数组进行分组输出&#xff0c;每组中的字符串都由相同的字符组成 题目&#xff1a; 将一个字符串数组进行分组输出&#xff0c;每组中的字符串都由相同的字符组成 举个例子&#xff1a;输入[“eat”,“tea”,“tan”,“ate”,“nat”,“bat”] 输出…

多个tomcat同时使用 不设置CATALINA_HOME环境变量

通常一台服务器只使用一个tomcat&#xff0c;设置一个CATALINA_HOME的环境变量。但有些时候需要一台服务器启动多个tomcat&#xff0c;那就不能设置CATALINA_HOME了&#xff01;因为会串~ 我们可以在对应tomcat的startup.bat启动脚本中&#xff0c;加入对应的CATALINA_HOME。 …

Cesium与Three相机同步(3)

Cesium与Three融合的案例demo <!DOCTYPE html> <html lang"en" class"dark"><head><meta charset"UTF-8"><link rel"icon" href"/favicon.ico"><meta name"viewport" content&q…

观察矩阵(View Matrix)、投影矩阵(Projection Matrix)、视口矩阵(Window Matrix)及VPM矩阵及它们之间的关系

V表示摄像机的观察矩阵&#xff08;View Matrix&#xff09;&#xff0c;它的作用是把对象从世界坐标系变换到摄像机坐标系。因此&#xff0c;对于世界坐标系下的坐标值worldCoord(x0, y0, z0)&#xff0c;如果希望使用观察矩阵VM将其变换为摄像机坐标系下的坐标值localCoord(x…

html+js+css在线倒计时

代码在图片后面 点赞加关注 谢谢大佬照顾&#x1f61c; 图例 时间到前 时间到后 源代码 <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width,…

设置单实例Apache HTTP服务器

配置仓库 [rootlocalhost ~]# cd /etc/yum.repos.d/ [rootlocalhost yum.repos.d]# vi rpm.repo仓库代码&#xff1a; [BaseOS] nameBaseOS baseurl/mnt/BaseOS enabled1 gpgcheck0[AppStream] nameAppStream baseurl/mnt/AppStream enabled1 gpgcheck0挂载 [rootlocalhost …

数据结构--单链表实现

欢迎光顾我的homepage 前言 链表和顺序表都是线性表的一种&#xff0c;但是顺序表在物理结构和逻辑结构上都是连续的&#xff0c;但链表在逻辑结构上是连续的&#xff0c;而在物理结构上不一定连续&#xff1b;来看以下图片来认识链表与顺序表的差别 这里以动态顺序表…

深度解析Java世界中的对象镜像:浅拷贝与深拷贝的奥秘与应用

在Java编程的浩瀚宇宙中&#xff0c;对象拷贝是一项既基础又至关重要的技术。它直接关系到程序的性能、资源管理及数据安全性。然而&#xff0c;提及对象拷贝&#xff0c;不得不深入探讨其两大核心类型&#xff1a;浅拷贝&#xff08;Shallow Copy&#xff09;与深拷贝&#xf…