React 源码揭秘 | Effect更新流程

前面的文章介绍了 hooks和commit流程,算是前置知识,这篇来讨论一下useEffect的原理。

useEffect用来处理副作用,比如网络请求,dom操作等等, 其本质也是个hooks,包含hooks的memorizedState, updateQueue, next 

Effect对象

useEfffect这个hooks的memorizedState存储的是Effect对象,其ts定义为

/** 定义effect */
export interface Effect {tags: HookEffectTag;// 依赖数组deps: HookDeps;// 传入的创建effectcreate: EffectCallback | null;// 清除effectdestory: EffectCallback | null;// next 用来连接在updateQueu中的Effect对象,Effect存在hook.memorizedState和fiber.updateQueue上next: Effect;
}

其中,tags定义为HookEffectTag,其为数字类型,定义在hookEffectTag.ts

export const Passive = 0b0010;
// 当前hook存在effect需要处理
export const HookHasEffect = 0b0001;export type HookEffectTag = number;

其中,Passive表示当前存在useEffect,而HookHasEffect表示当前useEffect在本次更新中需要处理。

deps就是依赖数组,类比useMemo useCallback

create是useEffect传入的create函数

destory是create函数返回的销毁函数

next表示下一个Effect对象

虽然useEffect本身也是个Hook,但是其hook节点的updateQueue是没有被使用的。useEffect将其Effect对象保存在memorizedState,并且使用了当前渲染Fiber上的updateQueue来存储Effect链,结构如下:

FCUpdateQueue

其中,对于函数类型的Fiber,其updateQueue的作用就是存储当前函数组件挂载的Effect对象,其使用的是FCUpdateQueue 是专门给函数组件提供呢更新队列,其继承updateQueue,多了lastEffect指针,指向Effect链(其本质也是环)。如下:

/** 函数组件专用的UpdateQueue增加了lastEffect 指向当前收集到的Effect */
export class FCUpdateQueue<State> extends UpdateQueue<State> {public lastEffect: Effect | null = null;
}

useEffect工作原理

每次更新的时候,mountEffect或者updateEffect都会检查当前的Effect是否被执行

  •  如果需要被执行,则将其放到Fiber对象的updateQueue中,并且将其tag设置为Passive|HookHasEffect
  • 如果不需要被执行,也需要将其推入updateQueue,只是其tag设置为Passive 代表有副作用,但是不执行

给当前的Fiber节点,设置PassiveEffect的flag

当commitMutation阶段时,会检查节点的Flag,如果包含passiveEffect,就会将当前节点的updateQueue推入root.pendingPassiveEffect数组中,你当前可以直接的将其理解为一个数组

在commitPassive阶段,会读取root.pendingPassiveEffect,依次执行FCUpdateQueue的内容

下面我们来看mount/updateEffect函数做了什么

mountEffect

挂载阶段,useEffect本质上执行的是mountEffect,其步骤如下:

1. 获取当前hook对象

2. 给当前的currentRenderingFiber.flag 设置PassiveEffect 表示当前Fiber对象包含Effect

3. 创建Effect对象,保存进creat函数和tag,并且将其分别保存到memorizedState和fiber.updateQueue

实现如下:

/** 挂载Effect */
function mountEffect(create: EffectCallback,deps: HookDeps
): EffectCallback | void {/** effect 在hook中的存储方式是:*  hook:*     memorizedState = Effect*     updateQueue = null*     next = nextHook*  fiber:*     updateQueue -> Effect1 -next-> Effect2 -...*/// 获取到hookconst hook = mountWorkInProgressHook();// 给fiber设置PassiveEffect 表示存在被动副作用(currentRenderingFiber as FiberNode).flags |= PassiveEffect;hook.memorizedState = pushEffect(// 初始化状态下,所有的useEffect都执行,所以这里flag设置为   Passive|HookHasEffectPassive | HookHasEffect,create,null,deps);
}

为什么要设置 fiber.flags?

因为effect真正执行的阶段在commit的passive阶段,而其收集阶段在commit的mutation阶段,所以render阶段时,需要在fiber上设置PassiveMask标记,表示当前fiber节点有副作用需要处理,commit阶段才会收集!

需要注意的是,mount阶段的tags为Passive | HookHasEffect 代表挂载阶段的所有Effect都会线执行一次!

其中,创建Effect和挂载到fiber.updateQueue的操作是pushEffect完成的!

pushEffect

pushEffect函数创建一个Effect对象,并且为其设置mask create destory deps依赖这些参数,可以将其看成是Effect对象的构造器。

同时,pushEffect还会将创建的Effect对象挂载到当前渲染Fiber的updateQueue上 并且返回这个Effect对象,实现如下:

/** 创建Effect对象,把effect加入到fiber.updateQueue 并且返回创建的Effect */
function pushEffect(tags: Flags,create: EffectCallback | null,destory: EffectCallback | null,deps: HookDeps
) {const effect: Effect = {tags,create,destory,deps: deps === undefined ? null : deps,next: null,};const updateQueue = currentRenderingFiber.updateQueue;if (!updateQueue || !(updateQueue instanceof FCUpdateQueue)) {// 创建一个FCUpdateQueueconst fcUpdateQueue = new FCUpdateQueue<Effect>();effect.next = effect; // 构建环fcUpdateQueue.lastEffect = effect;currentRenderingFiber.updateQueue = fcUpdateQueue;} else {// 已经存在 FCUpdateQueue 添加 后加环const fcUpdateQueue =currentRenderingFiber.updateQueue as FCUpdateQueue<Effect>;if (fcUpdateQueue.lastEffect) {effect.next = fcUpdateQueue.lastEffect.next;fcUpdateQueue.lastEffect.next = effect;fcUpdateQueue.lastEffect = effect;}}return effect;
}

pushEffect会检查,如果currentRenderingFiber.updateQueue为null (在renderWithHook时,会将updateQueue置为空以重置Effect) 则先创建一个FCUpdateQueue再加入,如果已经存在就直接加入。

updateEffect

更新阶段,由于fiber.updateQueue被清空,所以需要根据hook.memorizedState存储的Effect对象信息,重新判断哪些Effect需要重新执行。

判断是否要重新加入的依据就是deps,Effect存储了上一次的deps,updateEffect比较当前的deps和上一次prevDeps是否相等来判断是否需要重新执行。

判断方式为areInputDepsEqual, 前面有讲过。

  • 如果需要执行,则把Effect的tags改成 Passive | HookHasEffect 推入updateQueue
  • 如果不需要执行,则把Effect的tags改成Passive并且推入updateQueue

同时,在mount阶段,由于create函数还没有运行,所以无法获得destory函数,但是子啊update阶段,create函数已经在Passive Commit阶段运行过了,commit阶段会将create的返回值destory存入Effect对象中,直接获取即可,

也就是说,不论是mount还是update阶段,所有的Effect都会被挂载fiber.updateQueue上,其是否执行通过Effect.tag是否包含HookHasEffect这个HookEffectTag判断!

注意,更新阶段如果有需要执行的effect,也需要吧fiber.flags merge passiveEffect

实现如下:

/** 更新Effect */
function updateEffect(create: EffectCallback,deps: HookDeps
): EffectCallback | void {// 获取当前hookconst hook = updateWorkInProgressHook();const prevDeps = hook.memorizedState.deps;const destory = hook.memorizedState.destory;if (areHookInputsEqual(prevDeps, deps)) {// 相等 pushEffect 并且设置tag为Passive 被动副作用hook.memorizedState = pushEffect(Passive,create,// 前一个副作用hook的destorydestory,deps);} else {/** 不等 表示hook有Effect */hook.memorizedState = pushEffect(Passive | HookHasEffect, // 注意这里是 Passive 是Effect的tag 区分fiber的tag PassiveEffectcreate,// 前一个副作用hook的destorydestory,deps);}(currentRenderingFiber as FiberNode).flags |= PassiveEffect;
}

Effect收集阶段

effect的收集在commit阶段完成,我们上篇说了,如果某个Fiber节点存在需要执行的Effect,则其PassiveEffect的flag就会在completeWork阶段被冒泡到root节点。

commitRoot阶段,通过判断root节点是否有PassiveEffect的flags或subTreeFlags来判断是否开启Passive流程!

  /** 设置调度 执行passiveEffect */ty/** 真正执行会在commit之后 不影响渲染 *//** commit阶段会收集effect到root.pendingPassiveEffect */// 有删除 或者收集到Passive 都运行if ((finishedWork.flags & PassiveMask) !== NoFlags ||(finishedWork.subTreeFlags & PassiveMask) !== NoFlags) {// 调度副作用scheduler.scheduleCallback(PriorityLevel.NORMAL_PRIORITY,flushPassiveEffect.bind(null, root.pendingPassiveEffects));}

此时root.pendingPassiveEffect为空,需要在Mutation阶段收集

export const commitMutationEffects = commitEffect("mutation",MutationMask | PassiveMask,commitMutationEffectsOnFiber
);

可以看到commitMutationEffects中 mash包含了PassiveMask

pendingPassiveEffect的结构

root.pendingPassiveEffect的结构如下:

export interface PendingPassiveEffect {// 更新的effectupdate: Effect[];// 卸载的effectunmount: Effect[];
}

可以看到,其包含update和unmount两个数组,分别存储本次更新收集到的需要更新的effect和需要卸载的effect,在每次Passive Commit阶段接诉后,pendingPassEffect都会被置空! 

收集的几个地方

通过pendingPassEffect的结构就能看出,Effect的收集阶段分别发生在

1. 函数组件的卸载阶段

2. 函数组件的更新阶段

副作用的收集由commitPassiveEffect完成,其定义如下:

/** 收集被动副作用,这个函数可能会在*  1. commitMutationEffectsOnFiber调用*  2.  在delection时调用*/
function commitPassiveEffect(fiber: FiberNode,root: FiberRootNode,type: "update" | "unmount"
) {if (fiber.tag !== FunctionComponent) return;if (type === "update" && (fiber.flags & PassiveEffect) === NoFlags) return;const fcUpdateQueue = fiber.updateQueue as FCUpdateQueue<Effect>;if (fcUpdateQueue && fcUpdateQueue.lastEffect) {// 收集effectroot.pendingPassiveEffects[type].push(fcUpdateQueue.lastEffect);}
}

可以看到,其逻辑就是把当前存在PassiveEffect的Fiber节点的FCUpdateQueue.lastEffect 存入对应的update unmount数组,对于卸载阶段,是允许其flag不存在PassiveEffect的,因为卸载函数组件要destory其所有的effect

在commitMutationEfffectOnFiber 中,调用次函数,用来收集update阶段的effect

  if ((flags & PassiveEffect) !== NoFlags) {// 存在被动副作用commitPassiveEffect(finishedWork, root, "update");}

在commitDeletion阶段中,调用次函数用来收集unmount阶段的effect

    if (childToDelete.tag === FunctionComponent) {/** 函数组件的情况下,需要收集Effect */commitPassiveEffect(childToDelete, root, "unmount");}

在完成了Mutation Commit阶段之后,root.pendingPassiveEffect 就分别收集到了更新和卸载阶段的FCUpdateQueue

Effect的执行

effect的执行发生在Passive Commit阶段,我们前面说了,在Mutation阶段开始之前,会把一个flushPassiveEffect函数交给scheduler调度,以便在Mutation之后异步执行,其实现如下

function flushPassiveEffect(pendingPassiveEffect: PendingPassiveEffect) {// 处理卸载 把所有的Passive flag的effect都执行destorpendingPassiveEffect.unmount.forEach((unmountEffect) => {commitHookEffectListUnmount(Passive, unmountEffect);});pendingPassiveEffect.unmount = [];// 处理update 的destory flag为Passive|HookHasEffectpendingPassiveEffect.update.forEach((updateEffect) => {commitHookEffectListDestory(Passive | HookHasEffect, updateEffect);});// 处理update的create flag为Passive| HookHasEffectpendingPassiveEffect.update.forEach((updateEffect) => {commitHookEffectListCreate(Passive | HookHasEffect, updateEffect);});pendingPassiveEffect.update = [];
}

其实本质就是按顺序处理pendingPasiveEffect

首先处理卸载的effect -> 再处理更新的effect的destory -> 再处理更新effect的create

最后清空pendingPassiveEffect 以方便下次收集

我们首先看更新阶段处理create 其定义在commitHookEffectListCreate

function commitHookEffectList(flags: HookEffectTag,lastEffect: Effect | null,callback: (effect: Effect) => void
) {let currentEffect = lastEffect.next;do {if ((flags & currentEffect.tags) === flags) {// flag必须完全相等 执行callbackcallback(currentEffect);}currentEffect = currentEffect.next;} while (currentEffect !== lastEffect.next);
}/** 执行创建的effect */
export function commitHookEffectListCreate(flags: HookEffectTag,lastEffect: Effect | null
) {commitHookEffectList(flags, lastEffect, (effect) => {const create = effect.create;if (typeof create === "function") {// 设置destoryeffect.destory = create() as EffectCallback;}});
}

可以看到,其本质就是遍历pendingPassiveEffect.update,找到哪些包含HookHasEffect的Effect对象,执行其create函数,并且把返回值作为destory存入effect对象。

commitHookEffectListDestory,同样是遍历,找到包含HookHasEffect tag的Effect对象,执行其destory函数 如下:

/** 执行destory的effect */
export function commitHookEffectListDestory(flags: HookEffectTag,lastEffect: Effect | null
) {commitHookEffectList(flags, lastEffect, (effect) => {const destory = effect.destory;if (typeof destory === "function") {destory();}});
}

 最后在看卸载的情况,即commitHookEffectListUnmount 把unmount中所有的effect都执行一遍,并且去除其tag上的HookHasEffect标记(如果有的话)

/** 执行卸载的effect */
export function commitHookEffectListUnmount(flags: HookEffectTag,lastEffect: Effect | null
) {commitHookEffectList(flags, lastEffect, (effect) => {const destory = effect.destory;if (typeof destory === "function") {destory();}effect.tags &= ~HookHasEffect;});
}

这样就完成了一次更新的Effect的运行,最后清空pendingPassiveEffect即可

所以,Effect的执行一定是从下到上的,因为commit Mutation是在归的阶段执行,收集的Effect自然也是从下到上的!

 

 

 

 

 

 

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

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

相关文章

【Linux】vim 设置

【Linux】vim 设置 零、起因 刚学Linux&#xff0c;有时候会重装Linux系统&#xff0c;然后默认的vi不太好用&#xff0c;需要进行一些设置&#xff0c;本文简述如何配置一个好用的vim。 壹、软件安装 sudo apt-get install vim贰、配置路径 对所有用户生效&#xff1a; …

qt-C++笔记之QtCreator新建项目即Create Project所提供模板的逐个尝试

qt-C笔记之QtCreator新建项目即Create Project所提供模板的逐个尝试 code review! 文章目录 qt-C笔记之QtCreator新建项目即Create Project所提供模板的逐个尝试1.Application(Qt):Qt Widgets Application1.1.qmake版本1.2.cmake版本 2.Application(Qt):Qt Console Applicati…

Vue 项目中配置代理的必要性与实现指南

Vue 项目中配置代理的必要性与实现指南 在 Vue 前端项目的开发过程中&#xff0c;前端与后端地址通常不同&#xff0c;可能引发跨域问题。为了在开发环境下顺畅地请求后端接口&#xff0c;常常会通过配置**代理&#xff08;proxy&#xff09;**来解决问题。这篇文章将详细解析…

Linux运维命令-三剑客(grep awk sed)

目录 1.简介 2.命令详解 2.1.grep命令 2.1.1.功能 2.1.2.常见的使用场景及命令 2.2.awk命令 2.2.1.功能 2.2.2.常见的使用场景及命令 2.3.sed命令 2.3.1.功能 2.&#xff13;.2.常见的使用场景及命令 3.总结 1.简介 在Linux中&#xff0c;grep、awk、sed 命令常被称…

浅析 Redis 分片集群 Cluster 原理、手动搭建、动态伸缩集群、故障转移

大家好&#xff0c;我是此林。 之前的文章中分享了 Redis 集群方案的一种&#xff1a;主从集群哨兵机制 浅谈 Redis 主从集群原理&#xff08;一&#xff09;-CSDN博客 浅谈 Redis 主从复制原理&#xff08;二&#xff09;-CSDN博客 这种模式有什么缺点呢&#xff1f; 1. 虽…

Javaweb后端数据库多表关系一对多,外键,一对一

多表关系 一对多 多的表里&#xff0c;要有一表里的主键 外键 多的表上&#xff0c;添加外键 一对一 多对多 案例

PhotoLine绿色版 v25.00:全能型图像处理软件的深度解析

在图像处理领域,PhotoLine以其强大的功能和紧凑的体积,赢得了国内外众多用户的喜爱。本文将为大家全面解析PhotoLine绿色版 v25.00的各项功能,帮助大家更好地了解这款全能型的图像处理软件。 一、迷你体积,强大功能 PhotoLine被誉为迷你版的Photoshop,其体积虽小,但功能却…

Windows 11【1001问】修改主题隐藏或删除Win11桌面“了解此图片”

在<Windows 11【1001问】如何安装Windows 11>篇幅中我们第一安装完成Windows 11还未开始其他操作的时候会发现桌面上有一个“了解此图片”的图标是之前没见过的&#xff1b;而在Windows 11中&#xff0c;“了解此图片”图标是微软引入的一项功能&#xff0c;旨在让用户通…

Spring MVC框架二:创建第一个MVC程序

精心整理了最新的面试资料&#xff0c;有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 有两种方式 利用配置 1、利用IDEA新建一个Maven项目&#xff0c;添加一个web支持 2、导入常用的依赖 <dependencies><dependency><groupId>junit&…

go基本语法

跟Java比较学习。 hello word 示例代码 test1.go文件&#xff1a; // 包路径 package main// 导入模块&#xff0c;下面两种都行 import ("fmt" ) import "log"// main方法 func main() {log.Print("hello word !!!")fmt.Print("hello …

《零基础学会!如何用 sql+Python 绘制柱状图和折线图,数据可视化一看就懂》

在数据驱动的时代&#xff0c;MySQL 是最常用的关系型数据库管理系统之一&#xff0c;广泛应用于各类数据存储和处理场景。数据分析的过程不仅仅是收集数据&#xff0c;还包括数据的清洗、转换、查询以及最终的报告和可视化。在本文中&#xff0c;我们将通过实际案例来介绍如何…

【博资考2】网安学院-北航网安基础部分(简洁版)

【博资考2】网安学院-北航网安基础部分 写在最前面北航网安学院考纲&#xff08;二&#xff09;知识要点&#xff08;三&#xff09;快速梳理1. **单钥密码体制**2. **双钥密码体制**3. **消息认证与杂凑函数**4. **数字签名**5. **密码协议**6. **数字证书与公钥基础设施 (PKI…

【Transformer模型学习】第二篇:多头注意力机制

文章目录 0. 前言1. 注意力机制&#xff08;Attention&#xff09;概述2. Q、K、V矩阵是怎么来的&#xff1f;3. 缩放点积注意力&#xff08;Scaled Dot-Product Attention&#xff09;4. 多头注意力&#xff08;Multi-Head Attention&#xff09;5. 多头注意力的好处6. 总结 0…

网络运维学习笔记(DeepSeek优化版)002网工初级(HCIA-Datacom与CCNA-EI)子网划分与协议解析

文章目录 子网划分与协议解析1. VLSM与CIDR技术解析1.1 VLSM&#xff08;Variable Length Subnetwork Mask&#xff0c;可变长子网掩码&#xff09;1.2 CIDR&#xff08;Classless Inter-Domain Routing&#xff0c;无类域间路由&#xff09; 2. 子网划分方法与计算2.1 常规划分…

将VsCode变得顺手好用(1

目录 设置中文 配置调试功能 提效和增强相关插件 主题和图标相关插件 创建js文件 设置中文 打开【拓展】 输入【Chinese】 下载完成后重启Vs即可变为中文 配置调试功能 在随便一个位置新建一个文件夹&#xff0c;用于放置调试文件以及你未来写的代码&#xff0c;随便命名但…

在线疫苗预约小程序(论文源码调试讲解)

第4章 系统设计 用户对着浏览器操作&#xff0c;肯定会出现某些不可预料的问题&#xff0c;但是不代表着系统对于用户在浏览器上的操作不进行处理&#xff0c;所以说&#xff0c;要提前考虑可能会出现的问题。 4.1 系统设计思想 系统设计&#xff0c;肯定要把设计的思想进行统…

MySql数据库运维学习笔记

数据库运维常识 DQL、DML、DCL 和 DDL 是 SQL&#xff08;结构化查询语言&#xff09;中的四个重要类别&#xff0c;它们分别用于不同类型的数据库操作&#xff0c;下面为你简单明了地解释这四类语句&#xff1a; 1. DQL&#xff08;数据查询语言&#xff0c;Data Query Langu…

Redis 集群的三种模式:一主一从、一主多从和多主多从

本文记述了博主在学习 Redis 在大型项目下的使用方式&#xff0c;包括如何设置Redis主从节点&#xff0c;应对突发状况如何处理。在了解了Redis的集群搭建和相关的主从复制以及哨兵模式的知识以后&#xff0c;进而想要了解 Redis 集群如何使用&#xff0c;如何正确使用&#xf…

LangChain大模型应用开发:基于RAG实现文档问答

介绍 大家好&#xff0c;博主又来给大家分享知识了。随着大模型应用的不断发展&#xff0c;很多开发者都在探索如何更好地利用相关工具进行开发。那么这次给大家分享的内容是使用LangChain进行大模型应用开发中的基于RAG实现文档问答的功能。 好了&#xff0c;我们直接进入正…

零样本学习 zero-shot

1 是什么 2 如何利用零样本学习进行跨模态迁移&#xff1f; demo代码 安装clip pip install ftfy regex tqdm pip install githttps://github.com/openai/CLIP.git import torch import clip from PIL import Image# 加载 CLIP 模型 device "cuda" if torch.cuda.i…