React 源码揭秘 | 更新队列

前面几篇遇到updateQueue的时候,我们把它先简单的当成了一个队列处理,这篇我们来详细讨论一下这个更新队列。 有关updateQueue中的部分,可以见源码  UpdateQueue实现

Update对象

我们先来看一下UpdateQueue中的内容,Update对象,其实现如下:

/** 更新的Action 可以是State 也可以是函数 */
export type Action<State> = State | ((prevState: State) => State);
/** 定义Dispatch函数 */
export type Dispatch<State> = (action: Action<State>) => void;

/** 更新对象 */
export class Update<State> {
  next: Update<State>;
  action: Action<State>;
  lane: Lane; // 当前更新的优先级Lane
  constructor(action: Action<State>, lane: Lane) {
    this.action = action;
    this.next = null;
    this.lane = lane;
  }
}

其中,包含

  • action: Action对象,可以是任意类型,对应的我们在setState中传入的参数,如果传入一个函数,对应的是函数类型action,则运行函数得到状态值。如果不是函数,则直接将其作为状态值。
  • lane: 当前更新对应的优先级lane
  • next: 涉及到updateQueue的数据结构,指向下一个Update对象 

我们在很多地方都需要创建更新对象,比如dispatchSetState是,即你修改状态的时候

 初始化的时候,在updateContainer中,也会创建update对象

updateQueue - 环形链表 

updateQueue本质上是一个存储Update对象的数据结构,但是其不是一个普通的数组,其内部实现了一个环形链表用来存储Update对象,其定义如下

export class UpdateQueue<State> {
  shared: {
    pending: Update<State> | null;
  };
  /** 派发函数 */
  dispatch: Dispatch<State>;
  /** 基础队列 */
  baseQueue: Update<State> | null;
  /** 基础state */
  baseState: State;
...
}

其内部包含shared属性,指向一个对象,对象中包含pending对象,指向Update对象,如下图所示

其中,Update对象的next指针指向下一个Update对象,其组成一个环形链表,如图所示:

其中:

  • updateQueue.shared.pending指向最后一个Update节点
  • updateQueue.shared.pending.next 为第一个Update节点 

 为什么使用环形链表?

这里使用环形链表的一个好处是,其可以很方便的找到首位元素,可以方便的遍历链表,也可以方便的对两个链表进行拼接,这个在后面的baseQueue 和 baseState逻辑中会用到。

 enqueue入队

enqueue为UpdateQueue的类方法,其作用就是给队列插入Update对象,其实现如下:

 /** 入队,构造环状链表 */
  enqueue(update: Update<State>, fiber: FiberNode, lane: Lane) {
    if (this.shared.pending === null) {
      // 插入第一个元素,此时的结构为
      // shared.pending -> firstUpdate.next -> firstUpdate
      update.next = update;
      this.shared.pending = update;
    } else {
      // 插入第二个元素
      update.next = this.shared.pending.next;
      this.shared.pending.next = update;
      this.shared.pending = update;
    }
    /** 在当前的fiber上设置lane */
    fiber.lanes = mergeLane(fiber.lanes, lane);
    /** 在current上也设置lane 因为在beginwork阶段 wip.lane = NoLane 如果bailout 需要从current恢复 */
    const current = fiber.alternate;
    if (current) {
      current.lanes = mergeLane(current.lanes, lane);
    }
  }

我们用一个插入队列来演示插入过程:

// 假设有插入队列
enqueue(100)
enqueue(current => current + 1)
enqueue(200)

插入100, 100对应的pending.next指向自己,此时100对应的Update又是首节点也是尾节点

插入curr=>curr+1的update节点,此时首节点为pending.nexy也就是 curr=>curr+1 尾节点为100

插入200节点,此时首节点为200 尾节点为100 都是从pending.next的位置插入,如图

设置lane

enqueue方法除了传入更新对象,还需要传入更新所发生在的Fiber对象和对应的更新lane,其目的是在当前更新的Fiber上记录lane,其逻辑如下:

    /** 在当前的fiber上设置lane */
    fiber.lanes = mergeLane(fiber.lanes, lane);
    /** 在current上也设置lane 因为在beginwork阶段 wip.lane = NoLane 如果bailout 需要从current恢复 */
    const current = fiber.alternate;
    if (current) {
      current.lanes = mergeLane(current.lanes, lane);
    }

可以看到,当前更新的fiber节点的alternate节点的lanes也被设置了,这是为了先保存当前的lanes方便后面中短渲染 如bailout的时候能恢复当前fiber的lanes

processQueue - 处理更新

process函数的作用就是处理当前队列的所有更新,在不考虑优先级的情况下,其实现可以简化为如下代码:

  /** 处理任务 */
  process() {
    // 当前遍历到的update
    let memorizedState;
    let currentUpdate = this.baseQueue?.next;
    if (currentUpdate) {
      do {
         const currentAction = currentUpdate.action;
            if (currentAction instanceof Function) {
              /** Action是函数类型 运行返回newState */
              memorizedState = currentAction(memorizedState);
            } else {
              /** 非函数类型,直接赋给新的state */
              memorizedState = currentAction;
            }

        currentUpdate = currentUpdate.next;
      } while (currentUpdate !== this.baseQueue?.next);
    }
    return  memorizedState;
  }

即循环遍历整个环状链表,对action的类型进行检测,如果是函数则运行,如果是非函数直接把ation赋给memorizedState,最后将memorizedState返回即可! 

引入优先级lane

如果加入优先级lane的处理逻辑,process的处理逻辑会稍微有些复杂,我们看个例子

onClick={()=>{
    // 同步更新Lane = 1
    setvariable(100)
    
    startTransition(()=>{
        // 可以理解为 创建一个优先级lane=8的Update
        setVariable(curr=>curr+100)    
    })
   // 同步更新Lane = 1
    setVariable(curr => curr + 100)

}}

在一个onClick函数中,我们设置了三次setVariable函数,其中,第二次setter使用startTranstion包裹,这个函数由useTranstion hook提供,这个后面再讲,你可以先理解为,在这个startTransition包裹的setter对应的优先级都会被改成 8 即可 TransitionLane

此时,variable hook中的updateQueue对应的shared.pending队列如下:

由于队列中的优先级不同,我们一次只处理一个优先级的Update对象,对于其他优先级的对象需要进行跳过。

但是需要注意,被我们跳过的更新需要在后面的更新中被执行,并且,虽然我们通过优先级把一次更新拆分成了两次更新,但是最终的结果需要是一样的。

比如,第一次更新 

执行 action 100

跳过 curr=>curr+100 并且记住此时的状态100

执行curr => curr + 200

此时的结果为 300

第二次更新,需要从上次执行到的位置重新执行

执行curr=>curr+100 结果为200

执行 curr=>curr+200 (虽然此Update执行过了,但是为了保证结果一致,还需执行)结果为400

注意,虽然拆成了两次更新,但是最终更新的结果一定是和不加startTranstion按顺序执行的结果一样的!

这样我们就可以把高耗时的更新操作设置低优先级,先处理低耗时的更新,同时保证最终结果不变。

实现这样逻辑的算法如下:

准备一个memorizedState,记录当前updateQueue的状态值

准备一个baseState 用来记录第一个 跳过第一个Update时的状态值

准备一个baseQueue,用来记录本次更新跳过的更新对象 和 跳过更新之后的更新对象, 下一次更新就用这个baseQueue中的Update

遍历队列元素,使用isSubsetOfLanes来判断当前Update.lane是不是等于当前正在更新的lane(wipRenderedLane)

如果是则看baseQueue队列

   如果baseQueue队列为空, 则执行action,给memorizedState赋值

   如果baseQueue队列不为空, 则说明当前更新前面,已经有跳过的Update被加入到baseQueue了,那么其后面所有的Update对象都要加入baseQueue,则把当前Update对象克隆一份,并且设置优先级为Nolanes,以保证下次更细当前Update一定能被执行,推入baseQueue

并且,由于当前Update的lane是满足的,需要执行action,更新memorizedState

如果不是, 看updateQueue队列

 如果队列为空,此时为第一个跳过的Update对象,把当前的Update对象克隆一 份push到baseQueue中,并且把当前memorizedState赋给baseState,记录本次更新第一个跳过Update对应的状态,下次更新就从此开始

如果队列不为空,和上面一样,区别就是不赋baseState了,注意baseState只有第一次更新才设置

最后返回 memorizedState 并且把baseState baseQueue记录在当前updateQueue对象上,复习一下UpdateQueue的ts定义。

export class UpdateQueue<State> {
  shared: {
    pending: Update<State> | null;
  };
  /** 派发函数 */
  dispatch: Dispatch<State>;
  /** 基础队列 */
  baseQueue: Update<State> | null;
  /** 基础state */
  baseState: State;
...
}

下面我们画图来解释一下 Update队列如下:

Update List 
[action: 100,lane: 1]
[action: curr => curr + 100, lane: 8]
[action: curr = curr+ 200,lane: 1]

此时的updateQueue和状态如下: 

此时的root.pendinglanes 包含lane1 和 lane8 即SyncLane和TranstionLane

开始更新最高的优先级lane1 , 处理第一个Update,由于满足优先级,直接计算并且更新memorizedState = 100

继续处理到curr=>curr+100 此时lane=8 需要跳过,但是此时baseQueue为空,为第一个跳过的更新,需要baseState记录跳过之前的memorizedState = 100,并且克隆一份Update 推入baseQueue

 

继续处理curr=>curr+200 此时满足lane=1 但是由于baseQueue已经不为空,则后面所有的Update无论什么优先级,都需要克隆一份Update对象并且设置lanes为NoLane 推入baseQueue

同时需要计算action更新memorizedState为300

 

第一轮更新结束,此时状态为300,保存baseState和baseQueue并且删除shared.pending队列,因为已经用不上了。

第二轮更新 lane=8 此时从baseQueue中取出上次跳过的更新,继续处理,此时memorizedState被baseState初始化为100

 处理第一个更新,此时memorizedState=200

处理第二个更新,由于是任意Lanes&NoLanes === NoLanes 所以第二个update也满足优先级,更新memorizedState=400 此时完成更新 

 最终结果为400

两次更新,第一次更新值为300 第二次更新值为400 做到了过渡的作用

如果页面中包含逻辑,如果variable === 400 则渲染10000个li 此时如果不用startTranstion降低优先级,则更新variable到400的那次更新的优先级lane=1 那么此时如果有更高优先级任务来,则此次lane=1的更新无法被打断,导致页面卡住不动 影响用户体验。

如果更新到400的更新优先级为8 那么当更高优先级更新来的时候,此次大规模的更新会被打断,优先执行更高优先级更新(比如用户事件) 在高优先级任务执行完成之后,再执行这个大规模更新渲染,优化了用户体验!

连接baseQueue和pending

每一轮更新之后,pending对应的update环会被清空,但是当处理本次更新的时候,又有新的update被挂上,此时baseQueue和pending都有值

比如,在某次更新的useEffect中,设置了setVariable 此时的更新队列中又有新的更新了

此时就需要把baseQueue队列和pending队列连接,baseQueue队列在前

需要定义两个变量 baseFirst 和 pendingFirst 分别指向baseQueue和pending的对头,因为改变过pending/baseQueue.next 之后 就无法直接找到队头元素

第一步 设置baseQueue.next = pendingFirst 把baseQueue尾和pending头连接 如图

 第二步 Pending.next = baseFirst 此时pending队列的尾和baseQueue头连接 如图

此时 baseFirst 就是整个队列的头部了

说完了原理,我们看一下process方法的完整实现:

  /** 处理任务 */
  process(renderLane: Lane, onSkipUpdate?: (update: Update<any>) => void) {
    /** 获取baseQueue pending 完成拼接 */
    let baseState = this.baseState;
    let baseQueue = this.baseQueue;
    const currentPending = this.shared.pending;

    // 生成新的baseQueue过程
    if (currentPending !== null) {
      if (baseQueue !== null) {
        // 拼接两个队列
        // pending -> p1 -> p2 -> p3
        const pendingFirst = currentPending.next; // p1
        // baseQueue -> b1->b2->b3
        const baseFirst = baseQueue.next; // b1
        // 拼接
        currentPending.next = baseFirst; // p1 -> p2 -> p3 -> pending -> b1 -> b2 -> b3
        baseQueue.next = pendingFirst; //b1-> b2 -> b3 -> baseQueue -> p1 -> p2 -> p3
        // p1 -> p2 -> p3 -> pending -> b1 -> b2 -> b3 baseQueue
      }
      // 合并 此时 baseQueue -> b1 -> b2 -> b3 -> p1 -> p2 -> p3
      baseQueue = currentPending;

      // 覆盖新的baseQueue
      this.baseQueue = baseQueue;

      // pending可以置空了
      this.shared.pending = null;
    }

    // 消费baseQueue过程
    // 设置新的basestate和basequeue
    let newBaseState: State = baseState;
    let newBaseQueueFirst: Update<State> | null = null;
    let newBaseQueueLast: Update<State> | null = null;
    // 新的计算值
    let memorizedState: State = baseState;

    // 当前遍历到的update
    let currentUpdate = this.baseQueue?.next;
    if (currentUpdate) {
      do {
        const currentUpdateLane = currentUpdate.lane;
        // 看是否有权限
        if (isSubsetOfLanes(renderLane, currentUpdateLane)) {
          // 有权限
          if (newBaseQueueFirst !== null) {
            // 已经存在newBaseFirst 则往后加此次的update 并且将此次update的lane设置为NoLane 保证下次一定能运行
            const clone = new Update(currentUpdate.action, NoLane);
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }

          if (currentUpdate.hasEagerState) {
            memorizedState = currentUpdate.eagerState;
          } else {
            // 不论存不存在newBaseFirst 都要计算memorizedState
            const currentAction = currentUpdate.action;
            if (currentAction instanceof Function) {
              /** Action是函数类型 运行返回newState */
              memorizedState = currentAction(memorizedState);
            } else {
              /** 非函数类型,直接赋给新的state */
              memorizedState = currentAction;
            }
          }
        } else {
          // 无权限
          const clone = new Update(currentUpdate.action, currentUpdate.lane);
          if (onSkipUpdate) {
            onSkipUpdate(clone);
          }
          // 如果newBaseQueueFirst === null 则从第一个开始添加newbaseQueue队列
          if (newBaseQueueFirst === null) {
            newBaseQueueFirst = newBaseQueueLast = clone;
            // newBaseState到此 不在往后更新 下次从此开始
            newBaseState = memorizedState;
          } else {
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }
        }

        currentUpdate = currentUpdate.next;
      } while (currentUpdate !== this.baseQueue?.next);
    }

    if (newBaseQueueFirst === null) {
      // 此次没有update被跳过,更新newBaseState
      newBaseState = memorizedState;
    } else {
      // newbaseState不变 newBaseQueueFirst newBaseQueueLast 成环
      newBaseQueueLast.next = newBaseQueueFirst;
    }

    // 保存baseState和BaseQueue
    this.baseQueue = newBaseQueueLast;
    this.baseState = newBaseState;
    return { memorizedState };
  }

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解锁养生密码,拥抱健康生活

在快节奏的现代生活中&#xff0c;养生不再是一种选择&#xff0c;而是我们保持活力、提升生活质量的关键。它不是什么高深莫测的学问&#xff0c;而是一系列融入日常的简单习惯&#xff0c;每一个习惯都在为我们的健康加分。 早晨&#xff0c;当第一缕阳光洒进窗户&#xff0c…

muduo网络库2

Muduo网络库&#xff1a;底层实质上为Linux的epoll pthread线程池&#xff0c;且依赖boost库。 muduo的网络设计核心为一个线程一个事件循环&#xff0c;有一个main Reactor负载accept连接&#xff0c;然后把连接分发到某个sub Reactor(采用轮询的方式来选择sub Reactor)&…

【大语言模型】【整合版】DeepSeek 模型提示词学习笔记(散装的可以看我之前的学习笔记,这里只是归纳与总结了一下思路,内容和之前发的差不多)

以下是个人笔记的正文内容: 原文在FlowUs知识库上&#xff0c;如下截图。里面内容和这里一样&#xff0c;知识排版好看一点 一、什么是 DeepSeek 1. DeepSeek 简介 DeepSeek 是一家专注于通用人工智能&#xff08;AGI&#xff09;的中国科技公司&#xff0c;主攻大模型研发与…

AWQ和GPTQ量化的区别

一、前言 本地化部署deepseek时发现&#xff0c;如果是量化版的deepseek&#xff0c;会节约很多的内容&#xff0c;然后一般有两种量化技术&#xff0c;那么这两种量化技术有什么区别呢&#xff1f; 二、量化技术对比 在模型量化领域&#xff0c;AWQ 和 GPTQ 是两种不同的量…

ARM Cortex-M3 技术解析:核寄存器R1-R15介绍及使用

ARM Cortex-M3 技术解析&#xff1a;核寄存器R1-R15介绍及使用 作为嵌入式开发领域的经典处理器内核&#xff0c;ARM Cortex-M3&#xff08;CM3&#xff09;凭借其高效能、低功耗和丰富特性&#xff0c;在工业控制、物联网、消费电子等领域广泛应用。而内核寄存器是我们调试代…

ROS ur10机械臂添加140夹爪全流程记录

ROS ur10机械臂添加140夹爪 系统版本&#xff1a;Ubuntu20.04 Ros版本&#xff1a;noetic Moveit版本&#xff1a;moveit-noetic 参考博客&#xff1a; ur3robotiq ft sensorrobotiq 2f 140配置rviz仿真环境_有末端力传感器的仿真环境-CSDN博客 UR5机械臂仿真实例&#xf…

Web自动化之Selenium添加网站Cookies实现免登录

在使用Selenium进行Web自动化时&#xff0c;添加网站Cookies是实现免登录的一种高效方法。通过模拟浏览器行为&#xff0c;我们可以将已登录状态的Cookies存储起来&#xff0c;并在下次自动化测试或爬虫任务中直接加载这些Cookies&#xff0c;从而跳过登录步骤。 Cookies简介 …

【落羽的落羽 数据结构篇】树、二叉树

文章目录 一、树1. 树的概念和结构2. 树的相关术语 二、二叉树1. 概念与结构2. 满二叉树3. 完全二叉树4. 二叉树的性质5. 二叉树的存储结构 一、树 1. 树的概念和结构 之前我们学习了线性表&#xff0c;今天我们再来接触一种全新的数据结构——树。 树是一种非线性的数据结构…

数据结构(陈越,何钦铭) 第四讲 树(中)

4.1 二叉搜索树 4.1.1 二叉搜索树及查找 Position Find(ElementTyoe X,BinTree BST){if(!BST){return NULL;}if(X>BST->Data){return Find(X,BST->Right)}else if(X<BST->Data){return Find(X,BST->Left)}else{return BST;} } Position IterFind(ElementTyp…

【原创工具】同文件夹PDF文件合并 By怜渠客

【原创工具】同文件夹PDF文件合并 By怜渠客 原贴&#xff1a;可批量合并多个文件夹内的pdf工具 - 吾爱破解 - 52pojie.cn 他这个存在一些问题&#xff0c;并非是软件内自主实现的PDF合并&#xff0c;而是调用的pdftk这一工具&#xff0c;但楼主并没有提供pdftk&#xff0c;而…

Kafka系列之:记录一次源头数据库刷数据,造成数据丢失的原因

Kafka系列之:记录一次源头数据库刷数据,造成数据丢失的原因 一、背景二、查看topic日志信息三、结论四、解决方法一、背景 源头数据库在很短的时间内刷了大量的数据,部分数据在hdfs丢失了 理论上debezium数据采集不会丢失,就需要排查数据链路某个节点是否有数据丢失。 数据…

llama.cpp 一键运行本地大模型 - Windows

文章目录 llama.cpp 一键运行本地大模型 - Windows嘿&#xff0c;咱来唠唠 llama.cpp 这玩意儿&#xff01;gguf 格式是啥&#xff1f;咱得好好说道说道基座模型咋选&#xff1f;所需物料&#xff0c;咱得准备齐全咯核心命令&#xff0c;得记牢啦运行方式咋选&#xff1f;测试应…

BGP状态和机制

BGP邻居优化 为了增加稳定性,通常建议实验回环口来建立邻居。更新源:建立邻居和邻居所学习到的路由的下一跳。多跳:EBGP邻居建立默认选哟直连,因为TTL=1,如果非直连,必须修改TTL。命令备注peer 2.2.2.2 connect-interface lo1配置更新源peer 2.2.2.2 ebgp-max-hop 2配置T…

Holoens2开发报错记录02_通过主机获取彩色和深度数据流常见错误

01.E1696 E1696 无法打开源文件 “stdio.h” 解决方法&#xff1a; 更新一下SDK 1&#xff09;打开Visual Studio Installer&#xff0c;点击修改 2&#xff09;安装详细信息中自己系统对应的SDK&#xff0c;点击修改即可 02.WinError 10060 方法来源 解决方法&#xff1a…

labview关于计时器的使用

通过使用计时器函数&#xff0c;可以对采集和保存实现很好的控制&#xff0c;因为之前通过等待函数有出现程序卡死的情况&#xff0c;这里用到定时器函数来实现时间控制。 根据用户输入的采集频率&#xff0c;和采集的单位来确定是否上次采集的时间间隔减去这次计时器的时间是…

go语言环境下载与配置(Windows)

下载 Go下载 - Go语言中文网 - Golang中文社区 建议在D盘中创建文件夹安装到 D 盘 &#xff0c;方便进行管理&#xff0c;然后进行傻瓜式安装。 安装 验证安装 go version 安装成功 配置环境变量 winE --> 右击此电脑 --> 选择属性 --> 高级系统设置 --> 点击…

低延迟,高互动:EasyRTC的全场景实时通信解决方案

在数字化时代&#xff0c;实时通信技术已成为连接人与人、人与设备的重要桥梁。无论是在线教育、远程医疗、智能家居&#xff0c;还是企业协作&#xff0c;高效的实时互动体验都是提升效率和满意度的关键。而 EasyRTC&#xff0c;作为领先的实时通信解决方案&#xff0c;凭借其…

车载诊断架构 --- LIN节点路由转发注意事项

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…

浏览器深度解析:打造极速、安全、个性化的上网新体验

在数字化时代,浏览器作为我们获取信息、娱乐休闲的重要工具,其性能与功能直接影响着我们的上网体验。今天,我将为大家介绍一款备受好评的浏览器——Yandex浏览器,并深入解析其独特功能与优势,帮助大家更好地了解并选择这款上网神器。 一、知名公司背书,开源项目融合 Yan…