React Fiber 原理

431 阅读8分钟

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之前的区别主要是同步和异步的区别,

  1. React15是同步的,计算完dom diff后页面再重绘,假如dom diff时间过长,就会出现卡顿现象
  2. React16(Fiber架构)是异步的,dom diff和其他的工作过程React给了一个最大工作时间,如果超过了这个时间就先搁置,等闲置的时候重新开始工作。(本文章不讲调度,如果说的不全的或不对的,大家可以看看其他文章的内容) 所以结论是:React15程序本来有多快,就执行的有多快。React16,快的时候,看不出区别,但慢的时候会让程序看起来更快。

所以大家可以这样简单的想象一个2棵树的结构

  1. 虚拟DOM树,树状的数据结构
  2. 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'))

image.png 该过程做了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指向(这里有点绕,大家多看几遍)

image.png

beginWork

该过程简单来说2个作用

  1. 创建子fiber
  2. 计算副作用

该过程是基于当前工作中的fiber(也叫workingProgress)的updateQuque或者pendingProps不断调和出子fiber,这里多了一个概念(调和),在挂载阶段,大家可以理解为基于当前工作中的fiber,创建子fiber。并给他加一点副作用,这里又多了一个概念(副作用:既,DOM diff前后,莫些节点需要改变既:插入,更新,删除,这些改变就叫副作用)

从RootFiber调和出子fiber(key=div)

  1. 基于当前rootFiber的updateQueu属性创建子divFiber: 从rootFiber中的updateQueue取出虚拟dom,再创建一个fiber(div),并将来自updateQueue的children属性给divFiber(给下一个工作循环使用). 然后rootFiber的child指向divFiber,divFiber的return指向rootFiber。
  2. 计算当前节点的副作用:因为是挂载阶段所以给当前RootFiber挂一个effectTag属性等于2(插入节点)
  3. 完成上述2步后。workingprogress会指向divFiber image.png

从divFiber调和出pFiber(key=p)

  1. 基于当前divFiber中的pendingProps属性,创建pFiber:从divFiber中的pendingProps取出虚拟dom,再创建一个fiber(p),并将该虚拟dom的children属性给pFiber(给下一个工作循环使用),然后divFiber的child指向pFiber,pFiber的return指向divFiber。
  2. 计算当前节点的副作用:无副作用(对此处有疑问可以在评论区讨论)
  3. 完成上述2步后。workingprogress会指向pFiber

image.png

从pFiber调和出.spanFiber,labelFiber,iFiber

  1. 基于当前pFiber的pendingProps的chillren属性,for循环创建spanFiber:挂上chilrend: hello,labelFiber:挂上children: world,iFiber:挂上children fiber, 然后pFiber的child指向spanFiber,
  2. spanFiber的sibling指向labelFiber, labelFiber的sibling指向iFiber,spanFiber,labelFiber,iFiber的return都指向pFiber。
  3. 计算当前节点的副作用:无副作用(对此处有疑问可以在评论区讨论)
  4. 完成上述2步后。workingprogress会指向大儿子spanFiber image.png

从spanFiber调和出hello,结束beginWork

  1. 基于当前spanFiber 调和出子fiber,由于children是文本,react在文本下不会继续创建子fiber了,所以调和出null
  2. 计算副作用:无
  3. workingProgress指向spanFiber调和出的子fiber null
  4. 因为workingProgress指向null,beginWork结束

接下来进行completeWork

image.png

CompleteWork

改过程简单来说3个功能

  1. 创建真实DOM标签
  2. 将当前fiber的dom标签挂载上子fiber的DOM
  3. 把自己的副作用给爸爸

spanFiber completeWork工作

  1. 创建真实DOM标签span
  2. 挂载上子fiberDOM标签:无
  3. workingProgress 指向spanFiber的弟弟labelFiber
  4. 把自己的副作用给爸爸:无

image.png

labelFiber completeWork工作

  1. 创建真实DOM标签label
  2. 挂载上子fiberDOM标签:无
  3. workingProgress 指向labelFiber的弟弟iFiber
  4. 把自己的副作用给爸爸:无 image.png

iFiber completeWork工作

  1. 创建真实DOM标签i
  2. 挂载上子fiberDOM标签:无
  3. workingProgress 指向labelFiber的弟弟,没有弟弟,指向爸爸pFiber
  4. 把自己的副作用给爸爸:无 image.png

pFiber completeWork

  1. 创建真实DOM标签p
  2. 挂载上子fiberDOM标签:p.appendChld(span),p.appendChild(label),p.appendChild(i)。此处只挂载,没有渲染哦
  3. workingProgress 指向pFiber的弟弟,没有弟弟,指向爸爸divFiber
  4. 把自己的副作用给爸爸:无

image.png

divFiber completeWork

  1. 创建真实DOM标签div
  2. 挂载上子fiberDOM标签:div.appendChld(p)。此处只挂载,没有渲染哦
  3. workingProgress 指向divFiber的弟弟,没有弟弟,指向爸爸rootFiber
  4. 把自己的副作用给爸爸:无 image.png

rootFiber completeWork

  1. rootFiber不创建DOM标签因为他的containerInfo指向div#root
  2. 挂载上子fiberDOM标签:div#root.appendChld(div)。此处只挂载,没有渲染哦

image.png

提交

改工作是沿着副作用链,执行,挂载阶段,链条只有一个副作用,divFiber 插入到跟节点,太简单了,就不多写了,有问题在评论区里讨论

收集副作用

更新阶段和挂载阶段的工作循环不通的地方主要是,

  1. beginWork中进行dom diff发现fiber可以复用那么就不用重新创建fiber里,直接复用之前的fiber
  2. 收集的副作用更多了 其中收集副作用(把副作用给他的爸爸)是个很复杂的过程 下图中,左边的是更新前的DOM结构,剩下一个是更新后的DOM结构,然后流程图中是我根据2颗树的差异,推导出的每个节点上的副作用 因为把副作用给他的爸爸过程是发生在completeWork之后的才开始的, 所以fiber树中收集副作用发生的顺序依次是
  3. E的副作用给C
  4. F的副作用给C
  5. C的副作用给B
  6. D副作用给B
  7. H的副作用给G
  8. K的副作用给J
  9. J的副作用给G
  10. B的副作用给A
  11. G的副作用给A
  12. A给root 至于为什么是这个顺序了,没看清楚的同学请看前面的挂载工作循环中completeWork或者在评论区讨论

image.png

演示过程

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

image.png

F的副作用给C

image.png

C的副作用给B

image.png

D副作用给B

image.png

H的副作用给G

image.png

K的副作用给J

image.png

J的副作用给G

image.png

B的副作用给A

image.png

G的副作用给A

image.png

A给root

然后我们从rootFiber的firsetEffect沿着这个副作用链往下执行就行了。 接下来看一下更新过程中工作循环

更新阶段的工作循环

beginWork

  1. rootFiber 的current指向老的fiber树 (左边的fiber树)
  2. 根据rootFiber的current创建备份 生成右边树的最上面的rootFiber woringingProgress指向他,且用alternate相互指向对方
  3. 根据新的rootFiber调和出子divFiber,子divFiber 经过dom diff(key,和type)都没变发现可以复用,复用老fiber然后通过alternate相互指向, workingProgress指向divFiber
  4. 根据新的divFiber调和出子pFiber,子pFiber 经过dom diff(key,和type)都没变发现可以复用,复用老fiber然后通过alternate相互指向对方 ,workingProgress指向pFiber
  5. 根据新的pFiber调和出4子spanFiber,labelFiber,iFiber,SpanFiber,子pFiber 经过dom diff给2个spanFiber和iFiber挂上副作用,workingProgress指向spanFiber 6 beginWork结束

completeWork

创建或者复用dom节点 把副作用给他爸爸

其他的过程和挂载阶段差不多。

image.png