您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

Reconciler架构的双缓存树实现原理

时间:2023-03-28 14:54:42  来源:微信公众号  作者:前端时光屋

前言

通过​​上一篇​​文章的学习,了解了Fiber是什么,知道了Fiber节点可以保存对应的DOM节点。Fiber节点构成的Fiber Tree会对应DOM Tree。

前面也提到Fiber是一种新的调和算法,那么它是如何更新DOM节点的呢?

单个节点的创建更新流程

对于同一个节点,React 会比较这个节点的ReactElement与FiberNode,生成子FiberNode。并根据比较的结果生成不同标记(插入、删除、移动...),对应不同宿主环境API的执行。

 

图片

 

根据上面的Reconciler的工作流程,举一个例子:

比如:

mount阶段,挂载<div></div>。

  1. 先通过jsx("div")生成 React Element <div></div>。
  2. 生成的对应的fiberNode为null(由于是由于是挂载阶段,React还未构建组件树)。
  3. 生成子fiberNode(实际上就是这个div的fiber节点)。
  4. 生成Placement标记。

将<div></div>更新为<p></p>。

update阶段,更新将<div></div>更新为<p></p>。

  1. 先通过jsx("p")生成 React Element <p></p>。
  2. p与对应的fiberNode作比较(FiberNode {type: 'div'})。
  3. 生成子fiberNode为null。
  4. 生成对应标记Delement Placement。

用一张图解释上面的流程:

 

图片

 

当所有的ReactElement比较完后,会生成一颗fiberNode Tree,一共会存在两棵fiberNode Tree。

  • current:与视图中真实UI对应的fiberNode树。
  • workInProgress:触发更新后,正在reconciler中计算的fiberNode Tree(用于下一次的视图更新,在下一次视图更新后,会变成current Tree)。

这就是React中的"双缓存树"技术。

什么是"双缓存"?

双缓存技术是一种计算机图形学中用于减少屏幕闪烁和提高渲染性能的技术。

就好像你是一个画家,你需要在一个画布上绘制一幅画。在没有双缓存技术的情况下,你会直接在画布上作画。当你绘制一条线或一个形状时,观众会立即看到这个过程。如果你的绘画速度较慢,观众可能会看到画面的闪烁和变化,这会导致视觉上的不舒适。

引入双缓存技术就好比你有两个画布:一个是主画布,观众可以看到它;另一个是隐藏画布,观众看不到它。在这种情况下,你会在隐藏画布上进行绘画。当你完成一个阶段性的绘制任务后,你将隐藏画布上的图像瞬间复制到主画布上。观众只能看到主画布上的图像,而看不到隐藏画布上的绘制过程。这样,即使你的绘画速度较慢,观众也不会看到画面的闪烁和变化,从而获得更流畅的视觉体验。

使用双缓存技术时,计算机会在一个隐藏的缓冲区(后台缓冲区)上进行绘制,然后将绘制好的图像一次性复制到屏幕上(前台缓冲区)。这样可以减少屏幕闪烁,并提高渲染性能。

这种在内存中构建并直接替换的技术叫作双缓存。

React 中使用"双缓存"来完成Fiber Tree的构建与替换,对应着DOM Tree的创建于与更新。

双缓存Fiber树

Fiber架构中同时存在两棵Fiber Tree,一颗是"真实UI对应的 Fiber Tree"可以理解为前缓冲区。另一课是"正在内存中构建的 Fiber Tree"可以理解为后缓冲区,这里值宿主环境(比如浏览器)。

当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

双缓存树一个显著的特点就是两棵树之间会互相切换,通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

双缓存树切换的规则

React应用的根节点通过current指针在不同Fiber树的HostRootFiber根节点(ReactDOM.render创建的根节点)间切换。

  • 在 mount时(首次渲染),会根据jsx方法返回的React Element构建Fiber对象,形成Fiber树。
  • 然后这棵Fiber树会作为current Fiber应用到真实DOM上。
  • 在 update时(状态更新),会根据状态变更后的React Element和current Fiber作对比形成新的workInProgress Fiber树。
  • 即当workInProgress Fiber树构建完成交给Renderer(渲染器)渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树。
  • 然后workInProgress Fiber切换成current Fiber应用到真实DOM上,这就达到了更新的目的。

这一切都是在内存中发生的,从而减少了对DOM的直接操作。

每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新,这就是React中用的双缓存树切换规则。

Renderer 是一个与特定宿主环境(如浏览器 DOM、服务器端渲染、React Native 等)相关的模块。Renderer 负责将 React 组件树转换为特定宿主环境下的实际 UI。从而使 React 能够在多个平台上运行。

上面的语言可能有些枯燥,我们来画个图演示一下。

比如有下面这样一段代码,点击元素把div切换成p元素:

function App() {
  const [elementType, setElementType] = useState('div');

  const handleClick = () => {
    setElementType(prevElementType => {
      return prevElementType === 'div' ? 'p' : 'div';
    })
  }

  // 根据 elementType 的值动态创建对应的元素
  const Element = elementType;

  return (
    <div>
      <Element onClick={handleClick}>
        点击我切换 div 和 p 标签
      </Element>
    </div>
  )
}

const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />);

 

图片

 

接下来,我们分别从 mount(首次渲染)和 update(更新)两个角度讲解 Fiber 架构的工作原理。

mount 时 Fiber Tree的构建

mount 时有两种情况:

  1. 整个应用的首次渲染,这种情况发生首次进入页面时
  2. 某个组件的首次渲染,当 isShow 为 true时,Btn 组件进入 mount 首次渲染流程。
{isShow ? <Btn /> : null}

假如有这样一段代码:

function App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />)

mount 时上面的Fiber树构建过程如下:

  1. 首次执行ReactDOM.createRoot(root)会创建fiberRootNode。
  2. 接着执行到render(<App />)时会创建HostRootFiber,实际上它是一个HostRoot节点。

fiberRootNode 是整个应用的根节点,HostRootFiber 是 <App /> 所在组件树的根节点。

  1. 从HostRootFiber开始,以DFS(深度优先搜索)的的顺序遍历子节点,以及生成对应的FiberNode。
  2. 在遍历过程中,为FiberNode标记"代表不同副作用的 flags",以便后续在宿主环境中渲染的使用。

在上面我们之所以要区分fiberRootNode和HostRootFiber是因为在整个React应用程序中开发者可以多次多次调用render方法渲染不同的组件树,它们会有不同的HostRootFiber,但是整个应用的根节点只有一个,那就是fiberRootNode。

执行 ReactDOM.createRoot 会创建如图所示结构:

 

图片

 

mount 首屏渲染阶段

由于是首屏渲染阶段,页面中还没有挂载任何DOM节点,所以fiberRootNode.current指向的HostRootFiber没有任何子Fiber节点(即current Fiber树为空)。

当前仅有一个HostRootFiber,对应"首屏渲染时只有根节点的空白画面"。

<body>
  <div id="root"></div>
</body>

render 生成workInProgress树阶段

接下来进入render阶段,根据组件返回的JSX在内存中依次构建创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。

在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,(在首屏渲染时,只有HostRootFiber),也可以理解为首屏渲染时,它以自己的身份生成了一个workInProgress 树只不过还是HostRootFiber(HostRootFiber.alternate。

基于DFS(深度优先搜索)依次生成的workInProgress节点,并连接起来构成wip 树的过程如图所示:

 

图片

 

上图中已构建完的workInProgress Fiber树会在commit阶段被渲染到页面。

commit 阶段

等到页面渲染完成时,workInProgress Fiber树会替换之前的current Fiber树,进而fiberRootNode的current指针会指向新的current Fiber树。

完成双缓存树的切换工作,曾经的Wip Fiber树变为current Fiber树。

过程如图所示:

 

图片

 

update 时 Fiber Tree的更迭

  1. 接下来我们点击p节点触发状态改变。这会开启一次新的render阶段并构建一课新的workInProgress Fiber树。

和mount时一样,workInProgress Fiber的创建可以复用current Fiber树对应节点的数据,这个决定是否服用的过程就是Diff算法, 后面章节会详细讲解。

 

图片

 

  1. workInProgress Fiber树在render阶段完成构建后会进入commit阶段渲染到页面上。渲染完成后,workInProgress Fiber树变为current Fiber树。

 

图片

 

render 阶段的流程

接下来,我们来看看用原理,在源码中它是如何实现的。

Reconciler工作的阶段在 React 内部被称为 render 阶段,ClassComponent 的render函数、Function Component函数本身也都在 render 阶段被调用。

根据Scheduler调度的结果不同,render阶段可能开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。

也就是说React在执行render阶段的初期会依赖于Scheduler(调度器)的结果来判断执行哪个方法,比如Scheduler(调度器)会根据任务的优先级选择执行performSyncWorkOnRoot或performConcurrentWorkOnRoot方法。这取决于任务的类型和优先级。同步任务通常具有较高优先级,需要立即执行,而并发任务会在空闲时间段执行以避免阻塞主线程。

这里补充一下,调度器可能的执行结果,以用来判断执行什么入口函数:

如果不知道调度器的执行结构都有哪几类,可以跳过这段代码向下看:

现在还不需要学习这两个方法,只需要知道在这两个方法中会调用 performUnitOfWork方法就好。

// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

可以看到,它们唯一的区别就是是否会调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会终止循环,直到浏览器有空闲时间再继续遍历。

也就说当更新正在进行时,如果有 "更高优先级的更新" 产生,则会终端当前更新,优先处理高优先级更新。

高优先级的更新比如:"鼠标悬停","文本框输入"等用户更易感知的操作。

workInProgress代表当前正在工作的一个fiberNode,它是一个全局的指针,指向当前正在工作的 fiberNode,一般是workInProgress。

performUnitOfWork方法会创建下一个Fiber节点,并赋值给workInProgress,并将workInProgress与已经创建好的Fiber节点连接起来构成Fiber树。

这里为什么指向的是 workInProgress 呢? 因为在每次渲染更新时,即将展示到界面上的是 workInProgress 树,只有在首屏渲染的时候它才为空。

render阶段流程概览

Fiber Reconciler是从Stack Reconciler重构而来,通过递归遍历的方式实现可中断的递归。 因为可以把performUnitOfWork方法分为两部分:"递"和"归"。

"递" 阶段会从 HostRootFiber开始向下以 DFS 的方式遍历,为遍历到的每个fiberNode执行beginWork方法。该方法会根据传入的fiberNode创建下一级fiberNode。

当遍历到叶子元素(不包含子fiberNode)时,performUnitOfWork就会进入 "归" 的阶段。

"归" 阶段会调用completeWork方法处理fiberNode。当某个fiberNode执行完complete方法后,如果其存在兄弟fiberNode(fiberNode.sibling !== null),会进入其兄弟fiber的"递阶段"。如果不存在兄弟fiberNode,会进入父fiberNode的 "归" 阶段。

递阶段和归阶段会交错执行直至HostRootFiber的"归"阶段。到此,render阶段的工作就结束了。

举一个例子:

function App() {
  return (
    <div>
      <p>1229</p>
      jasonshu
    </div>
  )
}

const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />);

当执行完深度优先搜索之后形成的workInProgress树。

 

图片

 

图中的数组是遍历过程中的顺序,可以看到,遍历的过程中会从应用的根节点RootFiberNode开始,依次执行beginWork和completeWork,最后形成一颗Fiber树,每个节点以child和return项链。

注意:当遍历到只有一个子文本节点的Fiber时,该Fiber节点的子节点不会执行beginWork和completeWork,如图中的"jasonshu"文本节点。这是react的一种优化手段

刚刚提到:workInProgress代表当前正在工作的一个fiberNode,它是一个全局的指针,指向当前正在工作的 fiberNode,一般是workInProgress。

// 该函数用于调度和执行 FiberNode 树的更新和渲染过程
// 该函数的作用是处理 React 程序中更新请求,计算 FiberNode 树中的每个节点的变化,并把这些变化同步到浏览器的DOM中
function workLoop() {
  while (workInProgress !== null) {
    // 开始执行每个工作单元的工作
    performUmitOfWork(workInProgress);
  }
}

知道了beginWork和completeWork它们是怎样的流程后,我们再来看它是如何实现的:

这段代码主要计算FiberNode节点的变化,更新workInProgress,beginWork函数的最初运行也是在下面这个函数中,同时它也完成递和归两个阶段的操作。

// 在这个函数中,React 会计算 FiberNode 节点的变化,并更新 workInProgress
function performUmitOfWork(fiber: FiberNode) {
  // 如果有子节点,就一直遍历子节点
  const next = beginWork(fiber);
  // 递执行完之后,需要更新下工作单元的props
  fiber.memoizedProps = fiber.pendingProps;

  // 没有子节点的 FiberNode 了,代表递归到最深层了。
  if (next === null) {
    completeUnitOfWork(fiber);
  } else {
  // 如果有子节点的 FiberNode,则更新子节点为新的 fiberNode 继续执行
    workInProgress = next;
  }
}

在下面的函数中主要进行的操作:

// 主要进行归的过程,向上遍历父节点以及兄弟,更新它们节点的变化,并更新 workInProgress
function completeUnitOfWork(fiber: FiberNode) {
  let node: FiberNode | null = fiber;

  do {
    // 归:没有子节点之后开始向上遍历父节点
    completeWork(node);
    const sibling = node.sibling;
    if (sibling !== null) {
      // 有兄弟节点时,将指针指到兄弟节点
      workInProgress = sibling;
      return;
    }
    // 兄弟节点不存在时,递归应该继续往上指到父亲节点
    node = node.return;
    workInProgress = node;
  } while (node !== null);
}

到此,Reconciler的工作架构架子我们就搭完了。

接下来我们来讲在构建过程中每个Fiber节点具体是如何创建的呢?在下一篇会详细讲解beginWork和completeWork是如何实现的?会正式进入render阶段的实现了。



Tags:架构   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
美团外卖宣布新一轮组织架构调整:提拔多位年轻管理者,年轻化、扁平化成主基调
新浪科技讯 4月11日上午消息,继2月下旬、3月下旬两轮人员调整后,美团到店到家的组织架构调整仍在继续。近日,美团外卖以内部邮件的方式宣布了新一轮的组织调整:外卖事业部下成立...【详细内容】
2024-04-11  Search: 架构  点击:(4)  评论:(0)  加入收藏
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  Search: 架构  点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27  Search: 架构  点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  Search: 架构  点击:(10)  评论:(0)  加入收藏
京东小程序数据中心架构设计与最佳实践
一、京东小程序是什么京东小程序平台能够提供开放、安全的产品,成为品牌开发者链接京东内部核心产品的桥梁,致力于服务每一个信任我们的外部开发者,为不同开发能力的品牌商家提...【详细内容】
2024-03-27  Search: 架构  点击:(10)  评论:(0)  加入收藏
从 MySQL 到 ByteHouse,抖音精准推荐存储架构重构解读
ByteHouse是一款OLAP引擎,具备查询效率高的特点,在硬件需求上相对较低,且具有良好的水平扩展性,如果数据量进一步增长,可以通过增加服务器数量来提升处理能力。本文将从兴趣圈层...【详细内容】
2024-03-22  Search: 架构  点击:(24)  评论:(0)  加入收藏
全程回顾黄仁勋GTC演讲:Blackwell架构B200芯片登场
北京时间3月19日4时-6时,英伟达创始人黄仁勋在美国加州圣何塞SAP中心登台,发表GTC 2024的主题演讲《见证AI的变革时刻》。鉴于过去一年多时间里AI带来的生产力变革,以及英伟达...【详细内容】
2024-03-19  Search: 架构  点击:(17)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13  Search: 架构  点击:(6)  评论:(0)  加入收藏
有了LLM,所有程序员都将转变为架构师?
编译 | 言征 出品 | 51CTO技术栈(微信号:blog51cto)生成式人工智能是否会取代人类程序员?可能不会。但使用生成式人工智能的人类可能会,可惜的是,现在还不是时候。目前,我们正在见...【详细内容】
2024-03-07  Search: 架构  点击:(19)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  Search: 架构  点击:(36)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(12)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(17)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条