「React 深入」知悉Fiber,方能百战不殆~

news2025/1/24 2:19:57

React v16以上的版本引入了一个非常重要的概念,那就是fiber,实际上fiberreact团队花费两年的时间重构的架构,在之前的文章中也提及到了fiber,那么fiber架构究竟是什么,为什么要使用fiber

在正式开始前,我们可以先看看以下几个问题:

  • 什么是fiberfiber解决了什么问题?
  • fiber中,保存了哪些信息,这些信息的作用是什么?
  • 如何将jsx转化为fiber链表,又是如何将链表链接起来的
  • fiber是如何更新的?
  • fiberstack相比,主要解决了哪些方面的问题?

先附上今天的学习图谱,方便我们更好的理解:

走进 Fiber

什么是Fiber

在一个庞大的项目中,如果有某个节点发生变化,就会给diff带来巨大的压力,此时想要要找到真正变化的部分就会耗费大量的时间,也就是说此时,js会占据主线程去做对比,导致无法正常的页面渲染,此时就会发生页面卡顿、页面响应变差、动画、手势等应用效果差

为了解决这一问题,react团队花费两年时间,重写了react的核心算法reconciliation,在v16中发布,为了区分reconciler(调和器),将之前的reconciler称为stack reconciler,之后称作fiber reconciler(简称:fiber)

简而言之,fiber就是v16之后的虚拟DOMReact在遍历的节点的时候,并不是真正的DOM,而是采用虚拟的DOM

v16之前,React是如何遍历节点的?

我们先看看下面这张图:

遍历的顺序为:A => B => D => E => C => F => G

v16之前,react采用的是深度优先遍历去遍历节点,转化为代码为:

 const root = {key: 'A',children: [{key: 'B',children: [{key: 'D',},{key: 'E',},],},{key: 'C',children: [{key: 'F',},{key: 'G',},],},],};const walk = dom => dom.children.forEach(child => walk(dom));walk(root); 

可以看出这种遍历采取的递归遍历,如果这颗树非常的庞大,那么对应的栈也会越来越深,如果其中发生中断,那么整颗树都不能恢复。

也就是说,在传统的方法中,在寻找节点的过程中,花费了1s,那么这1s就是浏览器无法响应的,同时树越庞大,卡顿的效果也就越明显

所以在v16之前的版本,无法解决中断树庞大的问题

知悉fiber

在上面的介绍中,我们知道fiber实际上是一种核心算法,为了解决中断树庞大的问题,那么接下来我们先来了解下fiber

虚拟DOM是如何转化成fiber的

先看看最常见的一段jsx代码:

const Index = (props)=> {return (<div>大家好,我是小杜杜</div>);
} 

这段代码就是最普通的jsx,经过babel会编译成React.createElement的形式

再来看看绑定的结构:

ReactDOM.render(<App />,document.getElementById('root')
); 

之后会走一个beginWork的方法,这个方法会通过tag去判断这段代码的element对象,再之后会调用reconcileChildFibers函数,这个函数就是转化后的fiber结构

element、fiber和DOM元素 的关系

1.element对象就是我们的jsx代码,上面保存了propskeychildren等信息
2.DOM元素就是最终呈现给用户展示的效果
3.而fiber就是充当elementDOM元素的桥梁,简单的说,只要elemnet发生改变,就会通过fiber做一次调和,使对应的DOM元素发生改变

其中有一个tag,这个tag的类型就是判断 element对应那种的fiber,如:

beginWork 的入参

在这里我将这三个入参单独说一下,因为这三个参数比较重要,我们要有一个基础的概念

  • current:在视图层渲染的树
  • workInProgress:这个参数尤为重要,它就是在整个内存中所构建的 Fiber树,所有的更新都发生在workInProgress中,所以这个树是最新状态的,之后它将替换给current
  • renderLanes:跟优先级有关,优先级也是一块非常大的点,这里先不介绍

element 和 fiber 的对应表

在这里总结了一些比较常用的对照表,供大家参考:

fiberelement
FunctionComponent = 0函数组件
ClassComponent = 1类组件
IndeterminateComponent = 2初始化的时候不知道是函数组件还是类组件
HostRoot = 3根元素,通过reactDom.render()产生的根元素
HostPortal = 4ReactDOM.createPortal 产生的 Portal
HostComponent = 5dom 元素(如<div>
HostText = 6文本节点
Fragment = 7<React.Fragment>
Mode = 8<React.StrictMode>
ContextConsumer = 9<Context.Consumer>
ContextProvider = 10<Context.Provider>
ForwardRef = 11React.ForwardRef
Profiler = 12<Profiler>
SuspenseComponent = 13<Suspense>
MemoComponent = 14React.memo 返回的组件
SimpleMemoComponent = 15React.memo 没有制定比较的方法,所返回的组件
LazyComponent = 16<lazy />

fiber 保存了什么?

接下来我们看看fiber中保存了什么,如:

源码部分在packages/react-reconciler/src/ReactFiber.old.js中的FiberNode

在这里我们直接来看看对应的type(位置在同目录下的ReactInternalTypes.js)

然后简单的分为四个部分,分别是InstanceFiberEffectPriority

Instance

Instance:这个部分是用来存储一些对应element元素的属性

export type Fiber = {tag: WorkTag,// 组件的类型,判断函数式组件、类组件等(上述的tag)key: null | string, // keyelementType: any, // 元素的类型type: any, // 与fiber关联的功能或类,如<div>,指向对应的类或函数stateNode: any, // 真实的DOM节点...
} 

Fiber

Fiber:这部分内容存储的是关于fiber链表相关的内容和相关的propsstate

export type Fiber = {...return: Fiber | null, // 指向父节点的fiberchild: Fiber | null, // 指向第一个子节点的fibersibling: Fiber | null, // 指向下一个兄弟节点的fiberindex: number, // 索引,是父节点fiber下的子节点fiber中的下表ref:| null| (((handle: mixed) => void) & {_stringRef: ?string, ...})| RefObject,// ref的指向,可能为null、函数或对象pendingProps: any,// 本次渲染所需的propsmemoizedProps: any,// 上次渲染所需的propsupdateQueue: mixed,// 类组件的更新队列(setState),用于状态更新、DOM更新memoizedState: any, // 类组件保存上次渲染后的state,函数组件保存的hooks信息dependencies: Dependencies | null,// contexts、events(事件源) 等依赖mode: TypeOfMode, // 类型为number,用于描述fiber的模式 ...
} 

Effect

Effect:副作用相关的内容

export type Fiber = {... flags: Flags, // 用于记录fiber的状态(删除、新增、替换等) subtreeFlags: Flags, // 当前子节点的副作用状态 deletions: Array<Fiber> | null, // 删除的子节点的fiber nextEffect: Fiber | null, // 指向下一个副作用的fiber firstEffect: Fiber | null, // 指向第一个副作用的fiber lastEffect: Fiber | null, // 指向最后一个副作用的fiber...
} 

Priority

Priority: 优先级相关的内容

export type Fiber = {...lanes: Lanes, // 优先级,用于调度childLanes: Lanes,alternate: Fiber | null,actualDuration?: number,actualStartTime?: number,selfBaseDuration?: number,treeBaseDuration?: number,...
} 

链表之间如何连接的?

Fiber中我们看到有returnchildsibling这三个参数,分别指向父级、子级、兄弟,也就是说每个element通过这三个属性进行连接

举个栗子🌰:

const Index:React.FC<any> = (props)=> {return (<div>大家好,我是小杜杜<div>走进fiber的世界</div><p>收藏 === 学会</p></div>);
} 

那么按照之前讲的就会转化为:

Fiber 执行阶段

初始化(mount)阶段

在上文已经说过,react首次执行(初始化阶段)会以ReactDOM.render为入口,然后开始执行,由于调用的函数实在过多,这里我就简化一些,方便我们更好理解

createFiber

createFiber:这个函数会创建rootFiber,也就是react应用的根,会调用FiberNode函数来进行对应的构建工作

位置:packages/react-reconciler/src/ReactFiberRoot.old.js

const createFiber = function( tag: WorkTag,pendingProps: mixed,key: null | string,mode: TypeOfMode, ): Fiber {// $FlowFixMe: the shapes are exact here but Flow doesn't like constructorsreturn new FiberNode(tag, pendingProps, key, mode);
}; 

FiberNode就是上述讲的构造函数

createFiberRoot

createFiberRoot:它会调用FiberRootNode构造函数,创建fiberRoot,并且指向真正的根节点(root

位置:packages/react-reconciler/src/ReactFiberRoot.old.js

export function createFiberRoot(containerInfo: any,tag: RootTag,hydrate: boolean,hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);if (enableSuspenseCallback) {root.hydrationCallbacks = hydrationCallbacks;}const uninitializedFiber = createHostRootFiber(tag);root.current = uninitializedFiber; // 指向rootFiberuninitializedFiber.stateNode = root; // 指向fiberRootinitializeUpdateQueue(uninitializedFiber);return root;
} 

顺便看下FiberRootNode函数

beginWork

beginWork:这个函数正真走我们的jsx代码,也就是上面讲解的链表之间如何连接的部分

那么我们再把上面的图,拿出来,看看遍历的流程:

首先,我们要知道reactfiber结构的创建和更新都是深度优先遍历

1.首先会判断当前组件是类组件还是函数式组件,类组件tag为1,函数式为0
2.然后发现div标签,标记tag 为 5
3.发现div下包含三个部分,分别是,文本:大家好,我是小杜杜div标签p标签
4.首先遍历文本:大家好,我是小杜杜,下面无节点,标记tag 为 6
5.在遍历div标签,标记tag 为 5,此时下面有节点,所以对节点进行遍历,也就是文本走进fiber的世界,标记tag 为 6
6.同理最后遍历p标签

整个的流程就是这样,通过tag标记属于哪种类型,然后通过returnchildsibling这三个参数来判断节点的位置

更新(Update)阶段

接下来看看更新阶段,举个例子:

const Index:React.FC<any> = (props)=> {const [count, setCount] = useState(0)return (<div><div>数字:{count}</div><Button onClick={() => setCount(v => v + 1)} >点击</Button></div>);
} 

当我们点击按钮后,会走createWorkInProgress方法,这个方法会将创建一个新的workInProgress fiber,然后还是会深度优先遍历,对发生改变的fiber打上不同的flags副作用标签,然后通过副作用(Effect)中的nextEffectfirstEffectlastEffect等字短行程一个Effect List的链表

再来看对应的源码(位置在packages/react-reconciler/src/ReactFiber.old.js):

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {let workInProgress = current.alternate; //以alternate作为基础if (workInProgress === null) { //这里判断是初始化还是更新阶段workInProgress = createFiber(current.tag,pendingProps,current.key,current.mode,);workInProgress.elementType = current.elementType;workInProgress.type = current.type;workInProgress.stateNode = current.stateNode;...// 这部分是重置所有的副作用workInProgress.flags = current.flags & StaticMask;workInProgress.childLanes = current.childLanes;workInProgress.lanes = current.lanes;workInProgress.child = current.child;workInProgress.memoizedProps = current.memoizedProps;workInProgress.memoizedState = current.memoizedState;workInProgress.updateQueue = current.updateQueue; // 对依赖的克隆 const currentDependencies = current.dependencies; workInProgress.dependencies =currentDependencies === null? null: {lanes: currentDependencies.lanes,firstContext: currentDependencies.firstContext,}; workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref;
  ... return workInProgress;
} 

总的来说在更新阶段,更新阶段会将currentalternate作为基础,然后复制一部分,进行节点的更新,返回一个新的workInProgress

这里需要注意一点,current fiberworkInProgress fiber中的alternate是相互指向的,当新的 workInProgress fiber 创建完成后,fiberRootcurrent字段会从current fiber中的rootFiber变为workInProgress fiber中的rootFiber

fiber 带来后的变化

最后,我们再来看看更改前后的fiber图,发生了怎样的变化:

Stack Example

Fiber Example

可以看出来,fiber明显比stack要流畅很多,代表响应速度变快,宽度的变化也不会引发卡顿

对比Vue

我们经过上面的了解,已经知道React Fiber实际上是无差别刷新,他是将整个变化的树作为更改,而Vue是精确的将变化的节点进行替换,那是不是说Vue要强于React

其实这个话题非常有争议,就我个人而言,Vue的精确的替换也是具有代价的,至于两者孰强孰弱,作为一个小白也不好去多做评论,我们主要是学习思想,毕竟思想才是最重要的

React自身也会提供一些优化的方法,如useMemouseCallback等,我们一定要善用于这些API,帮助我们更好的去玩React

结语

React Fiber可以说是React的基石,很多方面都离不开它,学习fiber是不可缺少的一部分

实际上,这篇文章笔者已经推翻过两三次,主要原因是fiber实际上非常大,里面涉及的概念也十分琐碎,加上关于fiber的文章也非常多,我不知道该如何更好的去呈现出来

其次,这篇文章算是个入门级的fiberfiber比较难的概念都没有涉及到,阅读起来相对轻松一点,比较难的是优先级、调度、调和等模块

相比于更高级的模块,应该把架子搭起来,由浅入深,一点一点的慢慢啃,(如有不对的地方请在评论区留言指出~)

最后

最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

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

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

相关文章

网络空间安全——MS15_034漏洞验证与安全加固

网络空间安全——MS15_034漏洞验证与安全加固 靶机&#xff1a;windows 2008 安装好iis7.5 1.安装iis7.5,用127.0.0.1访问&#xff0c;将访问页面截图 2.用burpsuite 抓包分析是否存在ms15_034漏洞&#xff0c;抓包验证截图 3.调用msf相应的测试模块进行扫描&#xff0c;…

沥高科技冲刺创业板:拟募资5.45亿 为胡仲杰与岑婵芳夫妻店

雷递网 雷建平 12月20日上海沥高科技股份有限公司&#xff08;简称&#xff1a;“沥高科技”&#xff09;日前递交招股书&#xff0c;准备在深交所创业板上市。沥高科技计划募资5.45亿元&#xff0c;其中&#xff0c;1.79亿元用于航空航天用真空袋工艺材料生产项目&#xff0c;…

Talk预告 | 悉尼科技大学在读博士生胡思逸:MARLlib,全新的多智能体强化学习框架

本期为TechBeat人工智能社区第464期线上Talk&#xff01; 北京时间12月21日(周三)20:00&#xff0c;悉尼科技大学澳大利亚人工智能研究所ReLER实验室在读博士生——胡思逸的Talk将准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “MARLlib, 全新的多智能体…

2022 年 MathorCup 高校数学建模挑战赛A题

赛道 A&#xff1a;“58 到家”家政服务订单分配问题 “58 到家”是“58 同城”旗下高品质、高效率的上门家政服务平台&#xff0c;平 台向用户提供家政保洁、保姆、月嫂、搬家、维修等众多生活领域的服务。 在家政保洁场景中&#xff0c;用户在平台下单购买服务后&#xff0c;…

非零基础自学Golang 第13章 并发与通道 13.3 channel 13.3.1 channel类型 13.3.2 缓冲机制

非零基础自学Golang 文章目录非零基础自学Golang第13章 并发与通道13.3 channel13.3.1 channel类型13.3.2 缓冲机制第13章 并发与通道 13.3 channel goroutine运行在相同的地址空间&#xff0c;因此访问共享内存必须做好同步。 引用类型channel是CSP模式的具体体现&#xff…

游戏玩得好的AI,已经在看病救人了

一个游戏AI&#xff0c;怎么干起医生的活了&#xff1f; 而且这本事还是从打游戏的经验里总结来的。 喏&#xff0c;拿一张病理全片扫描图像&#xff0c;不用遍历所有高倍镜视野&#xff0c;也能找到病灶所在。 在它看来&#xff0c;这个过程和《我的世界》里伐木居然是类似的…

Diffusion惊艳应用大赏

文&#xff5c;白鹡鸰自从Diffusion模型兴起之后&#xff0c;AI绘画圈又迎来了一波猪突猛进式的强化&#xff0c;早几年还只是Ins的二次元/迪士尼风格滤镜&#xff0c;让人穿上不同服装当接头霸王。现在&#xff0c;你随便输几句话&#xff0c;模型就能刷刷刷给你吐出一大堆精美…

024 | 知行国学:全国领先的线上一对一国学教育平台 | 大学生创新训练项目申请书 | 极致技术工厂

公司产品 公司的产品包括两个部分的内容&#xff0c;一是模块化、智能化的课程体系——“诗书礼乐”快乐国学课程体系&#xff0c;二是梯队化、“以一带群”、联动型的师资培训体系——“知行者”大学生国学师资培训模式。 近年来&#xff0c;国家大力弘扬中华优秀传统文化&a…

在Docker中的ubuntu中安装Python3和Pip

下载载python3.7 apt-get update apt-get install python3.7 建立软链接 先删除旧的python连接。 删除后建立新的连接关系&#xff0c;例如linux下python3默认在/usr/bin/下 rm -rf /usr/bin/python ln -s /usr/bin/python3.7 /usr/bin/python 进入python3.7的lib中&#xf…

基于springboot的疫情防控系统java疫情防控物资管理平台源码和论文

系统开发技术介绍 2.1 MySQL数据库 社区疫情防控系统采用了一款开源免费的关系型数据库——MySQL数据库进行开发&#xff0c;因为它不收取任何费用&#xff0c;免费提供给各个开发者使用学习使用&#xff0c;使本次系统开发成本大大降低了。由于MySQL数据库体积小&#xff0c…

5.5 5个小红书月销售10W+的商家【玩赚小红书】

一、SWEETIEDOTS&#xff1a;蛋糕 简介 &#xff1a;甜点类商家&#xff0c;独创罐装厚卡龙、创新手工点心甜点&#xff0c;目前在小红书有2.7W粉丝&#xff0c;点赞评超7W&#xff0c;在简介上&#xff0c;告诉用户在薯店进行购买&#xff0c;客单价在98-108元。 内容&#x…

堆堆排序加强堆和堆有关的题一网打尽

堆结构、堆排序 堆结构的实现 堆结构就是用数组实现的完全二叉树结构 2)完全二叉树中如果每颗子树的最大值都在顶部就是大根堆 3)完全二叉树中如果每颗子树的最小值都在顶部就是小根堆 4)堆结构的向上调整和向下调整算法 向上调整 向下调整 5)堆结构某个元素的增大和减少 …

安卓手机独有的6个功能,个个都很实用,你用过几个呢?

安卓和苹果&#xff0c;你更喜欢哪一种&#xff1f;今天我整理了6个安卓独有的功能&#xff0c;不看不知道&#xff0c;一看安卓用户可能会吓一跳。因为这些功能真的很实用&#xff0c;但是苹果却没有这些功能。第一种&#xff1a;更改默认打开的应用 苹果系统无法直接更改默认…

【数据库】MVCC

Multi-version Concurrency Control DBMS 维护一个对象的多版本在数据库中。所以事务可以访问历史版本信息。 只依赖MVCC做不到可串行化&#xff1b; 一个事务在改对象的时候&#xff0c;留下一个历史版本&#xff0c;其他的事务可以读这个历史版本的数据。 读者不锁写着&a…

ch3_2多进程中寄存器的切换

运行中的程序在进行切换时&#xff0c; 可以分为以下两类&#xff1a; 线程&#xff1a; 只涉及指令的切换&#xff0c; 硬件资源没有切换&#xff1b;进程&#xff1a; 包含指令的切换&#xff0c;以及硬件资源的切换&#xff0c; 其中映射表便是一种内存资源。 1. 进程间的…

用3Dmax优化模型的方法,让你的效果图又快又好

3DMax是一个特别强大的建模软件&#xff0c;它具有无数需要小伙伴付出时间和练习才能掌握的特性和功能。 如果你已经能足够直观的掌握基础知识&#xff0c;并且已经能创造出很优秀的作品。这必然是一件值得高兴的事。 但是&#xff0c;在设计过程的妙处就在于&#xff0c;总是…

Prometheus集成Grafana(手动创建/通过模板创建Dashboard)

目录1. Grafana的基本介绍2. Centos7上安装Grafana2.1 下载解压2.2 修改conf/default.ini2.3 启动grafana3. Grafana各层级关系4. 添加Prometheus数据源5. 添加Dashborad和手动添加Row和Panel(方式一)6. 通过社区提供的模板创建Dashboard(方式二)1. Grafana的基本介绍 grafana…

自然语言基础之分词提取关键词基本操作

概述 从今天开始我们将开启一段自然语言处理 (NLP) 的旅程. 自然语言处理可以让来处理, 理解, 以及运用人类的语言, 实现机器语言和人类语言之间的沟通桥梁. 关键词 关键词 (keywords), 即关键词语. 关键词能描述文章的本质, 在文献检索, 自动文摘, 文本聚类 / 分类等方面有着…

我发现,被裁的一般都是情商低的人!网友:老板只看你有没有利用价值!

什么样的人容易被裁员&#xff1f;一位网友分享了自己的发现&#xff1a;被裁的一般都是情商比较低的。有人认同楼主&#xff0c;情商高能在一定程度上降低被裁的概率&#xff0c;大多数被裁的员工是因为情商不够&#xff0c;导致人际关系不好&#xff0c;才被辞退。有人说&…

JS如何读取对象内的一个属性两种方法

目录一&#xff1a;读取对象内的一个属性--可以用数组二&#xff1a;读取对象内的一个属性--可以用 .1、点的方式2、中括号的方式一&#xff1a;读取对象内的一个属性–可以用数组 二&#xff1a;读取对象内的一个属性–可以用 . let obj {name:张三,age:18,address:中国}1、…