【React Hooks原理 - useEffect、useLayoutEffect】

介绍

在实际React Hooks项目中,我们需要在项目的不同阶段进行一些处理,比如在页面渲染之前进行dom操作、数据获取、第三方加载等。在Class Component中存在很多生命周期能让我们完成这个操作,但是在React Hooks没有所谓的生命周期,但是它提供了useEffect、useLayoutEffect来让我们进行不同阶段处理,下面就从源码角度来聊聊这两个Hooks。【源码地址】

前提了解

同其他Hooks一样(useContext除外),在React18版本之后将其拆分为了mount、update两个函数,并由Dispatcher在不同阶段来执行不同函数。

// 挂载时
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,
};

下面介绍主要涉及到两个文件中内容:

  • react/src/ReactHooks.js
    这个文件主要是定义暴露的给用户实际使用的Hooks,即我们在组件中通过import { useXXX } from 'react'引入的Hooks。
  • react-reconciler/src/ReactFiberHooks.js
    该文件主要是React内部真正执行的Hooks函数,内部将Hooks拆分为了mount、update两个函数,并通过Dispatcher在不同阶段进行分发如上所示

useEffect

由于React18对于Hooks进行了重新组织,将其拆分为了挂载时和更新时,所以我们也从这两方面入手介绍。

mount挂载时

源代码文件路径:react/src/ReactHooks.js

export function useEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const dispatcher = resolveDispatcher();return dispatcher.useEffect(create, deps);
}

正如我们使用的那样,useEffect接受两个参数create、deps。然后通过dispatcher在不同阶段进行不同的处理即挂载时执行mountEffect,更新时执行updateEffect,通过上面的HooksDispatcherOnMount/HooksDispatcherOnUpdate映射。

React内部实现的Hooks代码都在react-reconciler/src/ReactFiberHooks.js文件下。(下面代码皆省略了DEV环境下的代码)

// react-reconciler/src/ReactFiberHooks.js
function mountEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {mountEffectImpl(PassiveEffect | PassiveStaticEffect,  // 定义的常量用于标记常规的副作用HookPassive, // 表示是被动类型的Hook常量,不需要用户主动调用create,deps,);
}function mountEffectImpl(fiberFlags: Flags,hookFlags: HookFlags,create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const hook = mountWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;currentlyRenderingFiber.flags |= fiberFlags;hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,createEffectInstance(),nextDeps,);
}function mountWorkInProgressHook(): Hook {const hook: Hook = {memoizedState: null,baseState: null,baseQueue: null,queue: null,next: null,};if (workInProgressHook === null) {// This is the first hook in the listcurrentlyRenderingFiber.memoizedState = workInProgressHook = hook;} else {// Append to the end of the listworkInProgressHook = workInProgressHook.next = hook;}return workInProgressHook;
}

从代码能看出,当我们调用useEffect之后,如果是首次挂载,React会通过dispatcher触发mountEffect函数,在其中调用了mountEffectImpl并传递了四个参数来对创建当前节点的hook。

  • PassiveEffect | PassiveStaticEffect: 用于标记副作用的常量,用于区分特性和用途。PassiveEffect 是用于标记常规的副作用,例如 useEffect 中定义的副作用。它表示这个副作用是在组件更新阶段执行的,但是不会阻塞浏览器的渲染。PassiveStaticEffect 是用于标记静态的副作用。表示这个副作用是静态的,不会在组件的多次渲染中发生变化,通常与静态数据相关。
  • HookPassive:标记Hook的类型常量,在 React 内部,不同类型的 Hook 会根据不同的标记和调度器进行处理。HookPassive 表示这个 Hook 是一种被动的类型,适用于大多数常规的 Hook 使用情况。
  • create:组件内使用useEffect包裹的函数
  • deps:useEffect包裹函数所依赖的参数

在mountEffect中将创建useEffect所需要的数据传递mountEffectImpl之后,就进行Hook的创建。在mountEffectImpl函数中主要做了这些操作:

  • 调用mountWorkInProgressHook函数,创建一个管理hooks的循环链表
  • 获取依赖nextDeps,以及设置该副作用的Flag
  • 通过pushEffect创建一个副作用链表,并保存在hook.memoizedState中
function pushEffect(tag: HookFlags,create: () => (() => void) | void,inst: EffectInstance, // 组件实例deps: Array<mixed> | null,
): Effect {const effect: Effect = {tag,create,inst,deps,// Circularnext: (null: any),};let componentUpdateQueue: null | FunctionComponentUpdateQueue =(currentlyRenderingFiber.updateQueue: any);if (componentUpdateQueue === null) {componentUpdateQueue = createFunctionComponentUpdateQueue();currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);componentUpdateQueue.lastEffect = effect.next = effect;} else {const lastEffect = componentUpdateQueue.lastEffect;if (lastEffect === null) {componentUpdateQueue.lastEffect = effect.next = effect;} else {const firstEffect = lastEffect.next;lastEffect.next = effect;effect.next = firstEffect;componentUpdateQueue.lastEffect = effect;}}return effect;
}

pushEffect主要是创建一个副作用循环链表,并将其挂载在当前渲染fiber节点的状态更新队列中。所以fiber.updateQueue.lastEffect 指向的就是pushEffect创建的副作用链表。

因为effect list是环状链表,updateQueue.lastEffect指向的最后元素,是因为这样有利于遍历时从起点开始,以及更好的插入effect

至此在挂载时,成功创建了hook链表和effect链表并挂载在当前渲染fiber节点的updateQueue中,后续通过在 Commit 阶段,React 会遍历 Effect list,执行相应的副作用操作。

update更新时

和挂载时类似,updateEffect会调用updateEffectImpl来进行更新处理。

function updateEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}function updateEffectImpl(fiberFlags: Flags,hookFlags: HookFlags,create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {const hook = updateWorkInProgressHook();const nextDeps = deps === undefined ? null : deps;const effect: Effect = hook.memoizedState;const inst = effect.inst;// currentHook is null on initial mount when rerendering after a render phase// state update or for strict mode.if (currentHook !== null) {if (nextDeps !== null) {const prevEffect: Effect = currentHook.memoizedState;const prevDeps = prevEffect.deps;if (areHookInputsEqual(nextDeps, prevDeps)) {hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);return;}}}currentlyRenderingFiber.flags |= fiberFlags;hook.memoizedState = pushEffect(HookHasEffect | hookFlags,create,inst,nextDeps,);
}

由上面可以知道pushEffect主要就是创建一个effect然后将其添加到fiber的更新队列中。而在更新时,通过areHookInputsEqual对比了前后渲染的依赖是否改变,然后通过pushEffect创建新的effect然后添加到更新队列,区别是当依赖改变时会将当前创建的新的hook的flag设置为HookHasEffect,表示当前副作用需要重新执行。

cleanup清除函数

在useEffect中返回一个函数,该函数会在每次组件更新前以及组件卸载前会执行,该函数称为清除函数。

useEffect(() => {console.log('useEffect');return () => {consoe.log('清除函数')}
}, [deps])

该函数会在commitHookEffectListMount函数中挂载到effect副作用上,并且在commitHookEffectListUnmount中执行,这两个函数都是在commit阶段进行的,文件路径为:packages/react-reconciler/src/ReactFiberCommitWork.js

commitHookEffectListMount 负责在副作用更新后重新执行副作用(即deps更新后会触发该函数执行):

function commitHookEffectListMount(tag: HookFlags,finishedWork: Fiber,
) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);let lastEffect: Effect | null = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {if ((effect.tag & tag) === tag) {// 执行副作用创建函数const create = effect.create;effect.destroy = create();}effect = effect.next;} while (effect !== firstEffect);}
}

从代码可以看出,在commitHookEffectListMount函数中,如果useEffect副作用中存在清除函数(即return的函数),则会挂载在副作用中,即 effect.destroy = create();

commitHookEffectListUnmount 负责在组件更新或卸载时清理副作用:

function commitHookEffectListUnmount(tag: HookFlags,finishedWork: Fiber,
) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);let lastEffect: Effect | null = updateQueue !== null ? updateQueue.lastEffect : null;if (lastEffect !== null) {const firstEffect = lastEffect.next;let effect = firstEffect;do {if ((effect.tag & tag) === tag) {// 执行清理函数const destroy = effect.destroy;if (destroy !== undefined) {destroy();}}effect = effect.next;} while (effect !== firstEffect);}
}

所以当组件卸载或者更新之前,会先执行清除函数然后在重新挂载新的清除函数。

useEffect的执行时机

上面说了在React Hooks中为了让我们能拥有类似Class Component生命周期一样对项目运行阶段进行监听并处理的功能,所以有了useEffect钩子,下面列举一下useEffect和Class Component生命周期的对应关系,帮助理解useEffect的执行时机。

  • 不写依赖数组: useEffect 会在每次渲染后执行,类似于 componentDidMount 和 componentDidUpdate 的结合。
  • 空依赖数组: useEffect 只在组件挂载和卸载时执行一次,类似于 componentDidMount 和 componentWillUnmount 的组合。
  • 带依赖数组: useEffect 只会在组件挂载时和依赖项发生变化时执行,类似于 componentDidUpdate 针对特定依赖项的变化。

可能有的同学看到不写依赖数组,会在每次渲染以及更新时都会执行,那这样和不适应useEffect包裹,直接在组件内声明有什么区别呢?下面也简单列举一下:

比较维度直接在函数内的代码useEffect 中的代码
执行时机在 React 调用组件函数期间同步执行,这意味着它会在 React 准备和生成新的虚拟 DOM 树时执行在 React 完成更新后(渲染并提交真实 DOM 变更后)异步执行,适合处理副作用,如数据获取、订阅、DOM 操作等
副作用管理不适合处理副作用,逻辑应该是纯函数的,不应引起副作用(例如,不直接操作 DOM)专为处理副作用设计,适合处理那些在组件渲染后需要进行的操作,如数据获取、DOM 更新或事件订阅
清理机制没有自动的清理机制。提供了一个清理函数,允许在组件卸载或下一次副作用执行之前进行清理工作。
性能优化每次渲染都会执行。如果不需要每次都执行,会造成不必要的性能开销。通过依赖数组控制执行频率,避免不必要的重新执行。

由表可以看出,主要区别在于副作用处理,和性能优化的区别。需要根据场景来决定如何使用,除非需要实时更新执行,否则一般不推荐在组件内直接写函数。

useLayoutEffect

上面聊了useEffect,下面来谈谈它的同胞兄弟useLayoutEffect,毕竟我们经常看到说使用useLayoutEffect可以有效解决在useEffect中操作状态/dom导致的屏幕闪缩问题。

同useEffect一样,useLayoutEffect也分为了mount、update。所以我们之间步如主题,从mountLayoutEffect开始。

从代码来看,在mount时useLayoutEffect和useEffect两者在语法上是一样的,都接受一个create函数(可包含cleanup函数),一个deps依赖数组,并都通过mountWorkInProgressHook来创建hook,然后通过pushEffect添加effect到hook中。更新时候也和useEffect大致一致,所以这里放在一起,重复代码则不再冗余贴上了。

// mount
function mountLayoutEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {let fiberFlags: Flags = UpdateEffect | LayoutStaticEffect;return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}// update
function updateLayoutEffect(create: () => (() => void) | void,deps: Array<mixed> | void | null,
): void {return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

区别就是在调用mountEffectImplupdateEffectImpl时传入的Flags不一样

// useEffect
const PassiveEffect = /* */ 0b000000001000;// useLayoutEffect
const UpdateEffect = /* */ 0b000000000100;
  • PassiveEffect 表示这是一个被动的副作用,它会在浏览器完成布局和绘制后执行。
  • UpdateEffect 表示这是一个同步的副作用,它会在所有 DOM 变更之后,浏览器绘制之前执行。

由此能看出useEffect是浏览器完成布局和绘制后异步执行,不影响渲染。而useLayoutEffect在所有 DOM 变更之后,浏览器绘制之前同步执行。

执行时机: DOM变更完成 -> useLayoutEffect(同步) -> 页面绘制 -> useEffect(异步)。 这也说明了为什么在useEffect中操作状态或者DOM时候,屏幕会闪缩(因为页面已经渲染,然后异步更新状态之后,会导致页面再次渲染时候存在时间差)。而useLayoutEffect能解决闪烁问题。(useLayoutEffect在页面还未绘制之前同步执行,修改状态之后再绘制到页面,对用户来说无感知,但是处理长任务时,会导致白屏问题)。

总结一下。两种区别主要是执行时机不同:

  • useEffect 使用 PassiveEffect 标志,确保副作用在浏览器完成绘制后异步执行。
  • useLayoutEffect 使用 UpdateEffect 标志,确保副作用在 DOM 更新后,浏览器绘制前同步执行。

总结

useEffect和useLayoutEffect在语法和代码组织上,逻辑大致相同。在mount阶段通过mountWorkInProgressHook 创建hook,pushEffect创建effect list并绑定在渲染fiber上。在update阶段通过updateEffectImpl调用updateWorkInProgressHook更新hook 列表,并通过areHookInputsEqual判断依赖是否变化,然后设置不同的Flag交给pushEffect创建新的effect,在执行时会根据设置的Flag来判断是否需要重新执行。

当状态更新时总的流程如下:

  • count 状态更新,组件重新渲染。
  • React 计算新的虚拟 DOM 并将其变更应用到实际 DOM。
  • useLayoutEffect 清除函数(如果存在)在 DOM 变更后立即同步执行。
  • useLayoutEffect 的新副作用在 DOM 变更后立即同步执行。
  • 浏览器绘制页面。
  • useEffect 清除函数(如果存在)在绘制完成后异步执行。
  • useEffect 的新副作用在绘制完成后异步执行。

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

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

相关文章

python - 类和对象

一.类 类名用大写字母开头 属性是类中的变量&#xff0c;方法是类中的函数 类、class关键字&#xff1a; >>> class Turtle: ... color green ... weight 10 ... legs 4 ... shell True ... mount 大嘴 ... def climb(self): ... …

从零开始读RocketMq源码(二)Message的发送详解

目录 前言 准备 消息发送方式 深入源码 消息发送模式 选择发送方式 同步发送消息 校验消息体 获取Topic订阅信息 高级特性-消息重投 选择消息队列-负载均衡 装载消息体发送消息 压缩消息内容 构造发送message的请求的Header 更新broker故障信息 异步发送消息 …

Open3D KDtree的建立与使用

目录 一、概述 1.1kd树原理 1.2kd树搜索原理 1.3kd树构建示例 二、常见的领域搜索方式 2.1K近邻搜索&#xff08;K-Nearest Neighbors, KNN Search&#xff09; 2.2半径搜索&#xff08;Radius Search&#xff09; 2.3混合搜索&#xff08;Hybrid Search&#xff09; …

ai native 模型微调

AI native 模型微调&#xff08;fine-tuning&#xff09;是指在预训练模型的基础上&#xff0c;通过对其参数进行进一步训练&#xff0c;使其在特定任务上表现更佳。以下是关于模型微调的一些基本步骤和概念&#xff1a; ### 1. 准备数据集 - **数据收集**&#xff1a;收集适用…

后端之路——登录校验前言(Cookie\ Session\ JWT令牌)

前言&#xff1a;Servlet 【登录校验】这个功能技术的基础是【会话技术】&#xff0c;那么在讲【会话技术】的时候必然要谈到【Cookie】和【Session】这两个东西&#xff0c;那么在这之前必须要先讲一下一个很重要但是很多人都会忽略的一个知识点&#xff1a;【Servlet】 什么是…

Oracle PL/SQL 循环批量执行存储过程

1. 查询存储过程 根据数据字典USER_OBJECTS查询出所有存储过程。 2. 动态拼接字符串&#xff08;参数等&#xff09; 根据数据字典USER_ARGUMENTS动态拼接参数。 3. 动态执行 利用EXECUTE IMMEDIATE动态执行无名块。 4. 输出执行信息 利用DBMS_OUTPUT.PUT_LINE输出执行成功与…

Android Gradle 开发与应用 (十): Gradle 脚本最佳实践

目录 1. 使用Gradle Kotlin DSL 1.1 什么是Gradle Kotlin DSL 1.2 迁移到Kotlin DSL 1.3 优势分析 2. 优化依赖管理 2.1 使用依赖版本管理文件 2.2 使用依赖分组 3. 合理使用Gradle插件 3.1 官方插件和自定义插件 3.2 插件管理的最佳实践 4. 任务配置优化 4.1 使用…

Oracle 19c 统一审计表清理

zabbix 收到SYSAUX表空间告警超过90%告警&#xff0c;最后面给出的清理方法只适合ORACLE 统一审计表的清理&#xff0c;传统审计表的清理SYS.AUD$不适合&#xff0c;请注意。 SQL> Col tablespace_name for a30 Col used_pct for a10 Set line 120 pages 120 select total.…

STM32实战篇:闪灯 × 流水灯 × 蜂鸣器

IO引脚初始化 即开展某项活动之前所做的准备工作&#xff0c;对于一个IO引脚来说&#xff0c;在使用它之前必须要做一些参数配置&#xff08;例如&#xff1a;选择工作模式、速率&#xff09;的工作&#xff08;即IO引脚的初始化&#xff09;。 IO引脚初始化流程 1、使能IO引…

LED灯的呼吸功能

"呼吸功能"通常是指 LED 灯的一种工作模式&#xff0c;它模拟人类的呼吸节奏&#xff0c;即 LED 灯的亮度会周期性地逐渐增强然后逐渐减弱&#xff0c;给人一种 LED 在"呼吸"的感觉。这种效果通常用于指示设备的状态或者简单地作为装饰效果。&#xff08;就…

Spring Boot Security自定义AuthenticationProvider

以下是一个简单的示例&#xff0c;展示如何使用AuthenticationProvider自定义身份验证。首先&#xff0c;创建一个继承自标准AuthenticationProvider的类&#xff0c;并实现authenticate方法。 import com.kamier.security.web.service.MyUser; import org.springframework.se…

【Adobe】Photoshop图层的使用

Adobe Photoshop(简称PS)中的图层是图像处理中一个核心概念,它允许用户以堆叠的方式组织图像的不同部分,从而实现对图像的复杂编辑和处理而不影响原始图像。以下是关于Adobe Photoshop图层的详细介绍: 一、图层的定义 图层就像是透明的纸张,你可以在上面绘制、添加图像…

YOLOv10改进 | EIoU、SIoU、WIoU、DIoU、FocusIoU等二十余种损失函数

一、本文介绍 这篇文章介绍了YOLOv10的重大改进&#xff0c;特别是在损失函数方面的创新。它不仅包括了多种IoU损失函数的改进和变体&#xff0c;如SIoU、WIoU、GIoU、DIoU、EIOU、CIoU&#xff0c;还融合了“Focus”思想&#xff0c;创造了一系列新的损失函数。这些组合形式的…

Android Init Language自学笔记

Android Init Language由五个元素组成&#xff1a;Acttions、Commands、Services、Options和Imports。 Actions和Services隐式声明了一个新的section。所以的Commands和Options都属于最近声明的section。 Services具有唯一的名称&#xff0c;如果重名会报错。 Actions Acti…

解决Spring Boot中的高可用性设计

解决Spring Boot中的高可用性设计 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. 高可用性设计概述 1.1 什么是高可用性&#xff1f; 高可用性指系统在面对各种故障和异常情况时&#xff0c;仍…

独立开发者系列(22)——API调试工具apifox的使用

接口的逻辑已经实现&#xff0c;需要对外发布接口&#xff0c;而发布接口的时候&#xff0c;我们需要能自己简单调试接口。当然&#xff0c;其实自己也可以写简单的代码调试自己的接口&#xff0c;因为其实就是简单的request请求或者curl库读取&#xff0c;调整请求方式get或者…

如果MySQL出现 “Too many connections“ 错误,该如何解决?

当你想要连接MySQL时出现"Too many connections" 报错的情况下&#xff0c;该如何解决才能如愿以偿呢&#xff1f;都是哥们儿&#xff0c;就教你两招吧&#xff01; 1.不想重启数据库的情况下 你可以尝试采取以下方法来解决&#xff1a; 增加连接数限制&#xff1a…

RxJava学习记录

文章目录 1. 总览1.1 基本原理1.2 导入包和依赖 2. 操作符2.1 创建操作符2.2 转换操作符2.3 组合操作符2.4 功能操作符 1. 总览 1.1 基本原理 参考文献 构建流&#xff1a;每一步操作都会生成一个新的Observable节点(没错&#xff0c;包括ObserveOn和SubscribeOn线程变换操作…

asp.netWebForm(.netFramework) CSRF漏洞

asp.netWebForm(.netFramework) CSRF漏洞 CSRF&#xff08;Cross-Site Request Forgery&#xff09;跨站请求伪造是一种常见的 Web 应用程序安全漏 洞&#xff0c;攻击者通过诱使已认证用户在受信任的网站上执行恶意操作&#xff0c;从而利用用户的身份 执行未经授权的操作。攻…

echarts实现3D饼图

先看下最终效果 实现思路 使用echarts-gl的曲面图&#xff08;surface&#xff09;类型 通过parametric绘制曲面参数实现3D效果 代码实现 <template><div id"surfacePie"></div> </template> <script setup>import {onMounted} fro…