当前位置: 首页 > news >正文

【React源码解析】深入理解react时间切片和fiber架构

时间切片

假如React一个更新需要耗时200ms,我们可以将其拆分为405ms的更新(后续会讲到如何拆分),然后每一帧里只花5ms来执行更新。那么,每一帧里不就剩余16.7 - 5 = 11.7ms的时间可以进行用户事件渲染等其他的js操作吗?如下所示:
image.png
那么这里就有两个问题:

  • 问题1:如何控制每一帧只执行5ms的更新?
  • 问题2:如何控制40个更新分配到每一帧里?

对于问题1比较容易,我们可以在更新开始时记录startTime,然后每执行一小段时间判断是否超过5ms。如果超过了5ms就不再执行,等下一帧再继续执行。

对于问题2,我们可以通过宏任务实现。比如5ms的更新结束了,那么我们可以为下一个5ms更新开启一个宏任务。浏览器则会将这个宏任务分配到当前帧或者是下一帧执行。

注意:
浏览器这一行为是内置的,比如设置 10000 个 setTimeout(fn, 0),并不会阻塞线程,而是浏览器会将这 10000 个回调合理分配到每一帧当中去执行。
比如:10000个个 setTimeout(fn, 0)在执行时,第一帧里可能执行了300个 setTimeout 回调,第二帧里可能执行了400个 setTimeout 回调,第 n 帧里可能执行了 200 个回调。浏览器为了尽量保证不掉帧,会合理将这些宏任务分配到帧当中去。

解决了上面两个问题,那么这个时候我们就有下面这种思路了:

  1. 更新开始,记录开始时间 startTime
  2. js 代码执行时,记录距离开始时间startTime是否超过了 5ms
  3. 如果超过了 5ms,那么这个时候就不应该再以同步的形式来执行代码了,否则依然会阻塞后续的代码执行。
  4. 所以这个时候我们需要把后续的更新改为一个宏任务,这样浏览器就会分配给他执行的时机。如果有用户事件进来,那么会执行用户事件,等用户事件执行完成后,再继续执行宏任务中的更新。

image.png
如上图所示,由于更新拆分成了一个个小的宏任务,从而使得click事件的回调有机会执行。

现在我们已经解决了更新阻塞的问题,接下来就需要解决如何将一个完整的更新拆分为多个更新,并且让它可以暂停等到click事件完成后再回来更新。

Fiber 架构

React传统的Reconciler是通过类似于虚拟DOM的方式来进行对比和标记更新。而虚拟DOM的结构不能很好满足将更新拆分的需求。因为它一旦暂停对比过程,下次更新时,很难找到上一个节点和下一个节点的信息,虽然有办法能找到,但是相对而言比较麻烦。所以,React团队引入了Fiber来解决这一问题。

每一个DOM节点对应一个Fiber对象,DOM树对应的Fiber结构如下:
image.png
(图片来自于这里)
Fiber通过链表的形式来记录节点之间的关系,它与传统的虚拟DOM最大的区别是多加了几个属性:

  • return表示父节点fiber
  • child表示子节点的第一个fiber
  • sibling表示下一个兄弟节点的fiber

通过这种链表的形式,可以很轻松的找到每一个节点的下一个节点或上一个节点。那么这个特性有什么作用呢?

结合上面提到的时间切片的思路,我们需要判断更新是否超过了5ms,我们以上面这棵Fiber树梳理一下更新的思路。从App Fiber开始:

  • 浏览器第一帧:
    • 记录更新开始时间startTime
    • 首先计算App节点,计算完成时,发现更新未超过5ms,继续更新下一个节点。
    • 计算div节点,计算完成时,发现更新超过了5ms,那么不会进行更新,而是开启一个宏任务。
  • 浏览器第二帧:
    • 上一帧最后更新的是div节点,找到下一个节点i am,计算该节点,发现更新未超过5ms,继续更新下一个节点。
    • 计算span节点,发现更新超过了5ms,那么不会进行更新,而是开启一个宏任务。
  • 浏览器第三帧:
    • 上一帧最后更新的是span节点,找到下一个节点KaSong,计算该节点,更新完成。

image.png

注:

  1. 实际的更新过程是 beginWork / completeWork 递与归的阶段,与这里有出入,这里仅做演示介绍。
  2. 这里的更新过程有可能不是第二帧和第三帧,而是在一帧里执行完成,具体需要看浏览器如何去分配宏任务。
  3. 更新过程分为 reconciler 和 commit 阶段,这里只会将 reconciler 阶段拆分。而 commit 阶段是映射为真实 DOM,无法拆分。

对应浏览器中的执行过程如下:
image.png
在这个过程中,每个节点计算完成后都会去校验更新时间是否超过了5ms,然后找到下一个节点继续计算,而双向链表恰恰是切合这种需求。

小结

通过上面的分析,我们可以总结成以下思路:

  1. 更新时遍历更新每一个节点,每更新一个Fiber节点后,会判断累计更新时间是否超过5ms
  2. 如果超过5ms,将下一个更新创建为一个宏任务,浏览器自动为其分配执行时机,从而不阻塞用户事件等操作。
  3. 如果更新的过程中,用户进行触发了点击事件,那么会在5ms与下一个5ms的间隙中去执行click事件回调。

通过以上步骤,我们能够将现有的同步更新转变为多个小更新分配到浏览器帧里,并且不会阻塞用户事件。接下来看看在React中实际是如何做到的。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 连锁管理系统如何兼批发和零售 连锁收银系统如何配合做好财务
  • 详解ImageNet著名子数据集ILSVRC2012基于Python的下载、解析及可视化
  • 6- 【JavaWeb】Maven管理项目
  • 如何调试本地npm package
  • 【前端】Flutter vs uni-app:性能对比分析
  • 系统架构设计师: 信息安全技术
  • Wazuh的安装和基本使用
  • 日志管理之Logrotate
  • 目标检测-YOLOv2
  • nefu 转专业到计算机相关专业(软件)的个人经历分享
  • STM32点亮第一个LED
  • 【RabbitMQ】工作模式
  • 如何模拟一个小程序项目打包的流程
  • 力扣题解2555
  • STM32F1+HAL库+FreeTOTS学习10——任务相关API函数使用
  • [数据结构]链表的实现在PHP中
  • 【跃迁之路】【477天】刻意练习系列236(2018.05.28)
  • 3.7、@ResponseBody 和 @RestController
  • 4. 路由到控制器 - Laravel从零开始教程
  • angular组件开发
  • Apache的80端口被占用以及访问时报错403
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • HomeBrew常规使用教程
  • passportjs 源码分析
  • Redis 中的布隆过滤器
  • Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比...
  • TypeScript实现数据结构(一)栈,队列,链表
  • vue:响应原理
  • vue中实现单选
  • 从PHP迁移至Golang - 基础篇
  • 大数据与云计算学习:数据分析(二)
  • 多线程 start 和 run 方法到底有什么区别?
  • 记一次删除Git记录中的大文件的过程
  • 全栈开发——Linux
  • 如何在 Tornado 中实现 Middleware
  • 少走弯路,给Java 1~5 年程序员的建议
  • 什么软件可以剪辑音乐?
  • 一加3T解锁OEM、刷入TWRP、第三方ROM以及ROOT
  • postgresql行列转换函数
  • 国内唯一,阿里云入选全球区块链云服务报告,领先AWS、Google ...
  • 如何用纯 CSS 创作一个货车 loader
  • #Linux(Source Insight安装及工程建立)
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #systemverilog# 之 event region 和 timeslot 仿真调度(十)高层次视角看仿真调度事件的发生
  • $(selector).each()和$.each()的区别
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • %3cli%3e连接html页面,html+canvas实现屏幕截取
  • (7)svelte 教程: Props(属性)
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (C++17) std算法之执行策略 execution
  • (libusb) usb口自动刷新
  • (Python第六天)文件处理
  • (二)换源+apt-get基础配置+搜狗拼音
  • (接口封装)