React Fiber 简单介绍
Fiber是什么
在react内部他是这样一个对象。
let fiberA = {
type: 'div',
tag: 3,
pendingProps: {
children: 'hello div'
},
effectTag: 2,
return: 父fiber,
child: 大儿子fiber,
sibling: 弟弟fiber,
firstEffect: null,
nextEffect: null,
lastEffect: null
}
至于每个属性代表什么意思,别急,先往后看。
Fiber的意义
Fiber架构和React 15之前的区别主要是同步和异步的区别,
- React15是同步的,计算完dom diff后页面再重绘,假如dom diff时间过长,就会出现卡顿现象
- React16(Fiber架构)是异步的,dom diff和其他的工作过程React给了一个最大工作时间,如果超过了这个时间就先搁置,等闲置的时候重新开始工作。(本文章不讲调度,如果说的不全的或不对的,大家可以看看其他文章的内容) 所以结论是:React15程序本来有多快,就执行的有多快。React16,快的时候,看不出区别,但慢的时候会让程序看起来更快。
所以大家可以这样简单的想象一个2棵树的结构
- 虚拟DOM树,树状的数据结构
- Fiber树,网状的数据结构(每个节点都都指针指向他想领的节点,看起来就像是一个网)
所以基于程序中断后,随时可以恢复的需求,Fiber的数据结构需要指向他相邻的节点,这样当上一个 工作过程指向自己的时候停止了,下一个过程开始的时候,就会接着从自己开始往上或者往下工作。
接下来介绍下fiber架构的工作过程
工作过程
初始化
假如页面的虚拟DOM结构是这样的,接下来介绍,挂载和更新过程中fiber是怎么工作的
let vnodeA = <div key='div'>
<p key='p'>
<span key='span'>hello</span>
<label key='label'>world</label>
<i key='i'>fiber</i>
</p>
</div>
ReactDOM.render(vnodeA, document.getElementById('root'))
该过程做了3件事
1. 创建RootFiber
也就是根节点div#root所指向的fiber其中,tag=3(表示根节点),type=div,key=div,然后以ReactDOM.render函数传入的第一个参数vnode,初始化一个更新队列,挂在rootFiber的updateQueue属性上。
2. 创建fiberRoot
创建一个对象fiberRoot其中current属性指向刚刚的RootFiber,containerInfo属性指向div#root这个dom节点
3. root节点的_reactRootContainer属性指向FiberRoot
接下来开始工作循环
挂载阶段工作循环
工作循环的核心逻辑分为以下几个过程
创建RootFiber备份
React需要判断当前的更新发生在哪些节点上,需要进行比较新,这里就需要新老fiber树进行比较,在初始化过程中我们创建了一个RootFiber(我们可以称他为老fiber,也是fiberRoot的current所指向的那个fiber),而基于老的rootFiber创建出来的备份,我们让workingProgress这个变量指向他,我们称他为当前正在工作中的fiber也叫新的rootFiber,新老2颗fiber树之间互相用alternate指向(这里有点绕,大家多看几遍)
beginWork
该过程简单来说2个作用
- 创建子fiber
- 计算副作用
该过程是基于当前工作中的fiber(也叫workingProgress)的updateQuque或者pendingProps不断调和出子fiber,这里多了一个概念(调和),在挂载阶段,大家可以理解为基于当前工作中的fiber,创建子fiber。并给他加一点副作用,这里又多了一个概念(副作用:既,DOM diff前后,莫些节点需要改变既:插入,更新,删除,这些改变就叫副作用)
从RootFiber调和出子fiber(key=div)
- 基于当前rootFiber的updateQueu属性创建子divFiber: 从rootFiber中的updateQueue取出虚拟dom,再创建一个fiber(div),并将来自updateQueue的children属性给divFiber(给下一个工作循环使用). 然后rootFiber的child指向divFiber,divFiber的return指向rootFiber。
- 计算当前节点的副作用:因为是挂载阶段所以给当前RootFiber挂一个effectTag属性等于2(插入节点)
- 完成上述2步后。workingprogress会指向divFiber
从divFiber调和出pFiber(key=p)
- 基于当前divFiber中的pendingProps属性,创建pFiber:从divFiber中的pendingProps取出虚拟dom,再创建一个fiber(p),并将该虚拟dom的children属性给pFiber(给下一个工作循环使用),然后divFiber的child指向pFiber,pFiber的return指向divFiber。
- 计算当前节点的副作用:无副作用(对此处有疑问可以在评论区讨论)
- 完成上述2步后。workingprogress会指向pFiber
从pFiber调和出.spanFiber,labelFiber,iFiber
- 基于当前pFiber的pendingProps的chillren属性,for循环创建spanFiber:挂上chilrend: hello,labelFiber:挂上children: world,iFiber:挂上children fiber, 然后pFiber的child指向spanFiber,
- spanFiber的sibling指向labelFiber, labelFiber的sibling指向iFiber,spanFiber,labelFiber,iFiber的return都指向pFiber。
- 计算当前节点的副作用:无副作用(对此处有疑问可以在评论区讨论)
- 完成上述2步后。workingprogress会指向大儿子spanFiber
从spanFiber调和出hello,结束beginWork
- 基于当前spanFiber 调和出子fiber,由于children是文本,react在文本下不会继续创建子fiber了,所以调和出null
- 计算副作用:无
- workingProgress指向spanFiber调和出的子fiber null
- 因为workingProgress指向null,beginWork结束
接下来进行completeWork
CompleteWork
改过程简单来说3个功能
- 创建真实DOM标签
- 将当前fiber的dom标签挂载上子fiber的DOM
- 把自己的副作用给爸爸
spanFiber completeWork工作
- 创建真实DOM标签span
- 挂载上子fiberDOM标签:无
- workingProgress 指向spanFiber的弟弟labelFiber
- 把自己的副作用给爸爸:无
labelFiber completeWork工作
- 创建真实DOM标签label
- 挂载上子fiberDOM标签:无
- workingProgress 指向labelFiber的弟弟iFiber
- 把自己的副作用给爸爸:无
iFiber completeWork工作
- 创建真实DOM标签i
- 挂载上子fiberDOM标签:无
- workingProgress 指向labelFiber的弟弟,没有弟弟,指向爸爸pFiber
- 把自己的副作用给爸爸:无
pFiber completeWork
- 创建真实DOM标签p
- 挂载上子fiberDOM标签:p.appendChld(span),p.appendChild(label),p.appendChild(i)。此处只挂载,没有渲染哦
- workingProgress 指向pFiber的弟弟,没有弟弟,指向爸爸divFiber
- 把自己的副作用给爸爸:无
divFiber completeWork
- 创建真实DOM标签div
- 挂载上子fiberDOM标签:div.appendChld(p)。此处只挂载,没有渲染哦
- workingProgress 指向divFiber的弟弟,没有弟弟,指向爸爸rootFiber
- 把自己的副作用给爸爸:无
rootFiber completeWork
- rootFiber不创建DOM标签因为他的containerInfo指向div#root
- 挂载上子fiberDOM标签:div#root.appendChld(div)。此处只挂载,没有渲染哦
提交
改工作是沿着副作用链,执行,挂载阶段,链条只有一个副作用,divFiber 插入到跟节点,太简单了,就不多写了,有问题在评论区里讨论
收集副作用
更新阶段和挂载阶段的工作循环不通的地方主要是,
- beginWork中进行dom diff发现fiber可以复用那么就不用重新创建fiber里,直接复用之前的fiber
- 收集的副作用更多了 其中收集副作用(把副作用给他的爸爸)是个很复杂的过程 下图中,左边的是更新前的DOM结构,剩下一个是更新后的DOM结构,然后流程图中是我根据2颗树的差异,推导出的每个节点上的副作用 因为把副作用给他的爸爸过程是发生在completeWork之后的才开始的, 所以fiber树中收集副作用发生的顺序依次是
- E的副作用给C
- F的副作用给C
- C的副作用给B
- D副作用给B
- H的副作用给G
- K的副作用给J
- J的副作用给G
- B的副作用给A
- G的副作用给A
- A给root 至于为什么是这个顺序了,没看清楚的同学请看前面的挂载工作循环中completeWork或者在评论区讨论
演示过程
function collectEffectList (returnFiber, completedWork) {
if (!returnFiber) {
return
}
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = completedWork.firstEffect
}
// 如果自己有列表尾,
if (completedWork.lastEffect) {
// 且父亲有列表尾
if (returnFiber.lastEffect) {
// 把自己身的effectList挂接到父亲的链表尾部
returnFiber.lastEffect.nextEffect = completedWork.firstEffect
}
returnFiber.lastEffect = completedWork.lastEffect
}
const flags = completedWork.flags
// 如果此完成的fiber有副作用,那么就需要添加到effectList里
if (flags) {
// 如果父fiber有lastEffect的话,说明父fiber已有effect链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork
} else {
returnFiber.firstEffect = completedWork
}
returnFiber.lastEffect = completedWork
}
}
let root = {name: 'root'}
let A = {name: 'A'}
let B = {name: 'B',}
let C = {name: 'C'}
let D = {name: 'D',flags: 'add'}
let E = {name: 'E',flags: 'update'}
let F = {name: 'F', flags: 'delete'}
let G = {name: 'G'}
let H = {name: 'H', flags: 'delete'}
let I = {name: 'I'}
let J = {name: 'J'}
let K = {name: 'K',flags: 'update'}
E的副作用给C
F的副作用给C
C的副作用给B
D副作用给B
H的副作用给G
K的副作用给J
J的副作用给G
B的副作用给A
G的副作用给A
A给root
然后我们从rootFiber的firsetEffect沿着这个副作用链往下执行就行了。 接下来看一下更新过程中工作循环
更新阶段的工作循环
beginWork
- rootFiber 的current指向老的fiber树 (左边的fiber树)
- 根据rootFiber的current创建备份 生成右边树的最上面的rootFiber woringingProgress指向他,且用alternate相互指向对方
- 根据新的rootFiber调和出子divFiber,子divFiber 经过dom diff(key,和type)都没变发现可以复用,复用老fiber然后通过alternate相互指向, workingProgress指向divFiber
- 根据新的divFiber调和出子pFiber,子pFiber 经过dom diff(key,和type)都没变发现可以复用,复用老fiber然后通过alternate相互指向对方 ,workingProgress指向pFiber
- 根据新的pFiber调和出4子spanFiber,labelFiber,iFiber,SpanFiber,子pFiber 经过dom diff给2个spanFiber和iFiber挂上副作用,workingProgress指向spanFiber 6 beginWork结束
completeWork
创建或者复用dom节点 把副作用给他爸爸
其他的过程和挂载阶段差不多。