关于react.js:React-Fiber架构原理剖析

43次阅读

共计 9637 个字符,预计需要花费 25 分钟才能阅读完成。

一、概述

在 React 16 之前,VirtualDOM 的更新采纳的是 Stack 架构实现的,也就是循环递归形式。不过,这种比照形式有显著的缺点,就是一旦工作开始进行就无奈中断,如果遇到利用中组件数量比拟宏大,那么 VirtualDOM 的层级就会比拟深,带来的后果就是主线程被长期占用,进而阻塞渲染、造成卡顿景象。

为了避免出现卡顿等问题,咱们必须保障在执行更新操作时计算时不能超过 16ms,如果超过 16ms,就须要先暂停,让给浏览器进行渲染,后续再继续执行更新计算。而 Fiber 架构就是为了反对“可中断渲染”而创立的。

在 React 中,Fiber 应用了一种新的数据结构 fiber tree,它能够把虚构 dom tree 转换成一个链表,而后再执行遍历操作,而链表在执行遍历操作时是反对断点重启的,示意图如下。

二、Fiber 架构

2.1 执行单元

官网介绍中,Fiber 被了解为是一种数据结构,然而咱们也能够将它了解为是一个执行单元。

Fiber 能够了解为一个执行单元,每次执行完一个执行单元,React Fiber 就会查看还剩多少工夫,如果没有工夫则将控制权让进来,而后由浏览器执行渲染操作。React Fiber 与浏览器的交互流程如下图。

能够看到,React 首先向浏览器申请调度,浏览器在执行完一帧后如果还有闲暇工夫,会去判断是否存在待执行工作,不存在就间接将控制权交给浏览器;如果存在就会执行对应的工作,执行完一个新的工作单元之后会持续判断是否还有工夫,有工夫且有待执行工作则会继续执行下一个工作,否则将控制权交给浏览器执行渲染,这个流程是循环进行的。

所以,咱们能够将 Fiber 了解为一个执行单元,并且这个执行单元必须是一次实现的,不能呈现暂停。并且,这个小的执行单元在执行完后计算之后,能够移交控制权给浏览器去响应用户,从而晋升了渲染的效率。

2.2 数据结构

在官网的文档中,Fiber 被解释为是一种数据结构,即链表构造。在链表构造中,每个 Virtual DOM 都能够示意为一个 fiber,如下图所示。

通常,一个 fiber 包含了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于下面的数据结构。

2.3 Fiber 链表构造

通过介绍,咱们晓得 Fiber 应用的是链表构造,精确的说是单链表树结构,详见 ReactFiber.js 源码。为了放便了解 Fiber 的遍历过程,上面咱们就看下 Fiber 链表构造。

在下面的例子中,每一个单元都蕴含了 payload(数据)和 nextUpdate(指向下一个单元的指针)两个元素,定义构造如下:

class Update {constructor(payload, nextUpdate) {
    this.payload = payload          //payload 数据
    this.nextUpdate = nextUpdate    // 指向下一个节点的指针
  }
}

接下来定义一个队列,把每个单元串联起来。为此,咱们须要定义两个指针:头指针 firstUpdate 和尾指针 lastUpdate,作用是指向第一个单元和最初一个单元,而后再退出 baseState 属性存储 React 中的 state 状态。

class UpdateQueue {constructor() {
    this.baseState = null  // state
    this.firstUpdate = null // 第一个更新
    this.lastUpdate = null // 最初一个更新
  }
}

接下来,再定义两个办法:用于插入节点单元的 enqueueUpdate()和用于更新队列的 forceUpdate()。并且,插入节点单元时须要思考是否曾经存在节点,如果不存在间接将 firstUpdate、lastUpdate 指向此节点即可。更新队列是遍历这个链表,依据 payload 中的内容去更新 state 的值

class UpdateQueue {
  //.....
  
  enqueueUpdate(update) {
    // 以后链表是空链表
    if (!this.firstUpdate) {this.firstUpdate = this.lastUpdate = update} else {
      // 以后链表不为空
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  
  // 获取 state,而后遍历这个链表,进行更新
  forceUpdate() {let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while (currentUpdate) {
      // 判断是函数还是对象,是函数则须要执行,是对象则间接返回
      let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = {...currentState, ...nextState}
      currentUpdate = currentUpdate.nextUpdate
    }
    // 更新实现后清空链表
    this.firstUpdate = this.lastUpdate = null
    this.baseState = currentState
    return currentState
  }
}

最初,咱们写一个测试的用例:实例化一个队列,向其中退出很多节点,再更新这个队列。

let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www'}))
queue.enqueueUpdate(new Update({ age: 10}))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1})))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1})))
queue.forceUpdate()
console.log(queue.baseState);       // 输入 {name:'www',age:12}

2.4 Fiber 节点

Fiber 框架的拆分单位是 fiber(fiber tree 上的一个节点),实际上拆分的节点就是虚构 DOM 的节点,咱们须要依据虚构 dom 去生成 fiber tree。Fiber 节点的数据结构如下:

{
    type: any,   // 对于类组件,它指向构造函数;对于 DOM 元素,它指定 HTML tag
    key: null | string,  // 惟一标识符
    stateNode: any,  // 保留对组件的类实例,DOM 节点或与 fiber 节点关联的其余 React 元素类型的援用
    child: Fiber | null, // 大儿子
    sibling: Fiber | null, // 下一个兄弟
    return: Fiber | null, // 父节点
    tag: WorkTag, // 定义 fiber 操作的类型, 详见 https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
    nextEffect: Fiber | null, // 指向下一个节点的指针
    updateQueue: mixed, // 用于状态更新,回调函数,DOM 更新的队列
    memoizedState: any, // 用于创立输入的 fiber 状态
    pendingProps: any, // 已从 React 元素中的新数据更新,并且须要利用于子组件或 DOM 元素的 props
    memoizedProps: any, // 在前一次渲染期间用于创立输入的 props
    // ……     
}

最终,所有的 fiber 节点通过以下属性:child,sibling 和 return 来形成一个树链表。
其余的属性还有 memoizedState(创立输入的 fiber 的状态)、pendingProps(将要扭转的 props)、memoizedProps(上次渲染创立输入的 props)、pendingWorkPriority(定义 fiber 工作优先级)等等就不在过多的介绍了。

2.5 API

2.5.1 requestAnimationFrame

requestAnimationFrame 是浏览器提供的绘制动画的 API,它要求浏览器在下次重绘之前(即下一帧)调用指定的回调函数以更新动画。

例如,应用 requestAnimationFrame 实现正方形的宽度加 1px,直到宽度达到 100px 进行,代码如下。

<body>
  <div id="div" class="progress-bar"></div>
  <button id="start"> 开始动画 </button>
</body>

<script>
  let btn = document.getElementById('start')
  let div = document.getElementById('div')
  let start = 0
  let allInterval = []

  const progress = () => {
    div.style.width = div.offsetWidth + 1 + 'px'
    div.innerHTML = (div.offsetWidth) + '%'
    if (div.offsetWidth < 100) {let current = Date.now()
      allInterval.push(current - start)
      start = current
      requestAnimationFrame(progress)
    }  
  }

  btn.addEventListener('click', () => {
    div.style.width = 0
    let currrent = Date.now()
    start = currrent
    requestAnimationFrame(progress)
  })
</script>

运行下面的代码,就能够看到浏览器会在每一帧运行完结后,将 div 的宽度加 1px,直到 100px 为止。

2.5.2 requestIdleCallback

requestIdleCallback 也是 Fiber 的根底 API。requestIdleCallback 能使开发者在主事件循环上执行后盾和低优先级的工作,而不会影响提早要害事件,如动画和输出响应。失常帧工作实现后没超过 16ms,阐明有多余的闲暇工夫,此时就会执行 requestIdleCallback 里注册的工作。

具体的执行流程是,开发者采纳 requestIdleCallback 办法注册对应的工作,告知浏览器工作的优先级不高,如果每一帧内存在闲暇工夫,就能够执行注册的这个工作。另外,开发者是能够传入 timeout 参数去定义超时工夫的,如果到了超时工夫,那么浏览器必须立刻执行,应用办法如下:

window.requestIdleCallback(callback, { timeout: 1000})。

浏览器执行完办法后,如果没有剩余时间了,或者曾经没有下一个可执行的工作了,React 应该偿还控制权,并同样应用 requestIdleCallback 去申请下一个工夫片。具体的流程如下图:

其中,requestIdleCallback 的 callback 中会接管到默认参数 deadline,其中蕴含了以下两个属性:

  • timeRamining:返回以后帧还剩多少工夫供用户应用。
  • didTimeout:返回 callback 工作是否超时。

三、Fiber 执行流程

Fiber 的执行流程总体能够分为渲染和调度两个阶段,即 render 阶段和 commit 阶段。其中,render 阶段是可中断的,须要找出所有节点的变更;而 commit 阶段是不可中断的,只会执行操作。

3.1 render 阶段

此阶段的次要工作就是找出所有节点产生的变更,如节点的新增、删除、属性变更等。这些变更,React 统称为副作用,此阶段会构建一棵 Fiber tree,以虚构 Dom 节点的维度对工作进行拆分,即一个虚构 Dom 节点对应一个工作,最初产出的后果是副作用列表(effect list)。

3.1.1 遍历流程

在此阶段,React Fiber 会将虚构 DOM 树转化为 Fiber tree,这个 Fiber tree 是由节点形成的,每个节点都有 child、sibling、return 属性,遍历 Fiber tree 时采纳的是后序遍历办法,遍历的流程如下:
从顶点开始遍历;
如果有大儿子,先遍历大儿子;如果没有大儿子,则示意遍历实现;
大儿子:a. 如果有弟弟,则返回弟弟,跳到 2 b. 如果没有弟弟,则返回父节点,并标记实现父节点遍历,跳到 2 d. 如果没有父节点则标记遍历完结

上面是后序遍历的示意图:

此时,树结构的定义如下:

const A1 = {type: 'div', key: 'A1'}
const B1 = {type: 'div', key: 'B1', return: A1}
const B2 = {type: 'div', key: 'B2', return: A1}
const C1 = {type: 'div', key: 'C1', return: B1}
const C2 = {type: 'div', key: 'C2', return: B1}
const C3 = {type: 'div', key: 'C3', return: B2}
const C4 = {type: 'div', key: 'C4', return: B2}

A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4

module.exports = A1

3.1.2 收集 effect list

接下来,就是收集节点产生的变更,并将后果转化成一个 effect list,步骤如下:

  1. 如果以后节点须要更新,则打 tag 更新以后节点状态(props, state, context 等);
  2. 为每个子节点创立 fiber。如果没有产生 child fiber,则完结该节点,把 effect list 归并到 return,把此节点的 sibling 节点作为下一个遍历节点;否则把 child 节点作为下一个遍历节点;
  3. 如果有剩余时间,则开始下一个节点,否则等下一次主线程闲暇再开始下一个节点;
  4. 如果没有下一个节点了,进入 pendingCommit 状态,此时 effect list 收集结束,完结。

如果用代码来实现的话,首先须要遍历子虚构 DOM 元素数组,为每个虚构 DOM 元素创立子 fiber。

const reconcileChildren = (currentFiber, newChildren) => {
  let newChildIndex = 0
  let prevSibling // 上一个子 fiber

  // 遍历子虚构 DOM 元素数组,为每个虚构 DOM 元素创立子 fiber
  while (newChildIndex < newChildren.length) {let newChild = newChildren[newChildIndex]
    let tag
    // 打 tag,定义 fiber 类型
    if (newChild.type === ELEMENT_TEXT) { // 这是文本节点
      tag = TAG_TEXT
    } else if (typeof newChild.type === 'string') {  // 如果 type 是字符串,则是原生 DOM 节点
      tag = TAG_HOST
    }
    let newFiber = {
      tag,
      type: newChild.type,
      props: newChild.props,
      stateNode: null, // 还未创立 DOM 元素
      return: currentFiber, // 父亲 fiber
      effectTag: INSERT, // 副作用标识,包含新增、删除、更新
      nextEffect: null, // 指向下一个 fiber,effect list 通过 nextEffect 指针进行连贯
    }
    if (newFiber) {if (newChildIndex === 0) {currentFiber.child = newFiber // child 为大儿子} else {prevSibling.sibling = newFiber // 让大儿子的 sibling 指向二儿子}
      prevSibling = newFiber
    }
    newChildIndex++
  }
}

该办法会收集 fiber 节点下所有的副作用,并组成 effect list。每个 fiber 有两个属性:

  • firstEffect:指向第一个有副作用的子 fiber。
  • lastEffect:指向最初一个有副作用的子 fiber。

而咱们须要收集的就是两头 nextEffect,最终造成一个单链表。

// 在实现的时候要收集有副作用的 fiber,组成 effect list
const completeUnitOfWork = (currentFiber) => {
  // 后续遍历,儿子们实现之后,本人能力实现。最初会失去以上图中的链条构造。let returnFiber = currentFiber.return
  if (returnFiber) {
    // 如果父亲 fiber 的 firstEffect 没有值,则将其指向以后 fiber 的 firstEffect
    if (!returnFiber.firstEffect) {returnFiber.firstEffect = currentFiber.firstEffect}
    // 如果以后 fiber 的 lastEffect 有值
    if (currentFiber.lastEffect) {if (returnFiber.lastEffect) {returnFiber.lastEffect.nextEffect = currentFiber.firstEffect}
      returnFiber.lastEffect = currentFiber.lastEffect
    }
    const effectTag = currentFiber.effectTag
    if (effectTag) { // 阐明有副作用
      // 每个 fiber 有两个属性:// 1)firstEffect:指向第一个有副作用的子 fiber
      // 2)lastEffect:指向最初一个有副作用的子 fiber
      // 两头的应用 nextEffect 做成一个单链表
      if (returnFiber.lastEffect) {returnFiber.lastEffect.nextEffect = currentFiber} else {returnFiber.firstEffect = currentFiber}
      returnFiber.lastEffect = currentFiber
    }
  }
}

最初,再定义一个递归函数,从根节点登程,把全副的 fiber 节点遍历一遍,最终产出一个 effect list。

const performUnitOfWork = (currentFiber) => {beginWork(currentFiber)
  if (currentFiber.child) {return currentFiber.child}
  while (currentFiber) {completeUnitOfWork(currentFiber)  
    if (currentFiber.sibling) {return currentFiber.sibling}
    currentFiber = currentFiber.return 
  }
}

3.2 commit 阶段

commit 阶段须要将上阶段计算出来的须要解决的副作用一次性执行,此阶段不能暂停,否则会呈现 UI 更新不间断的景象。此阶段须要依据 effect list,将所有更新都 commit 到 DOM 树上。

3.2.1 依据 effect list 更新视图

此阶段,依据一个 fiber 的 effect list 列表去更新视图,此次只列举了新增节点、删除节点、更新节点的三种操作。

const commitWork = currentFiber => {if (!currentFiber) return
  let returnFiber = currentFiber.return
  let returnDOM = returnFiber.stateNode // 父节点元素
  if (currentFiber.effectTag === INSERT) {  // 如果以后 fiber 的 effectTag 标识位 INSERT,则代表其是须要插入的节点
    returnDOM.appendChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === DELETE) {  // 如果以后 fiber 的 effectTag 标识位 DELETE,则代表其是须要删除的节点
    returnDOM.removeChild(currentFiber.stateNode)
  } else if (currentFiber.effectTag === UPDATE) {  // 如果以后 fiber 的 effectTag 标识位 UPDATE,则代表其是须要更新的节点
    if (currentFiber.type === ELEMENT_TEXT) {if (currentFiber.alternate.props.text !== currentFiber.props.text) {currentFiber.stateNode.textContent = currentFiber.props.text}
    }
  }
  currentFiber.effectTag = null
}

写一个递归函数,从根节点登程,依据 effect list 实现全副更新。

/**
* 依据一个 fiber 的 effect list 更新视图
*/
const commitRoot = () => {
  let currentFiber = workInProgressRoot.firstEffect
  while (currentFiber) {commitWork(currentFiber)
    currentFiber = currentFiber.nextEffect
  }
  currentRoot = workInProgressRoot // 把以后渲染胜利的根 fiber 赋给 currentRoot
  workInProgressRoot = null
}

3.2.2 视图更新

接下来,就是循环执行工作,当计算实现每个 fiber 的 effect list 后,调用 commitRoot 实现视图更新。

const workloop = (deadline) => {
  let shouldYield = false // 是否须要让出控制权
  while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1 // 如果执行完工作后,剩余时间小于 1ms,则须要让出控制权给浏览器}
  if (!nextUnitOfWork && workInProgressRoot) {console.log('render 阶段完结')
    commitRoot() // 没有下一个工作了,依据 effect list 后果批量更新视图}
  // 申请浏览器进行再次调度
  requestIdleCallback(workloop, { timeout: 1000})
}

到此,依据收集到的变更信息实现了视图的刷新操作,Fiber 的整个刷新流程也就实现了。

四、总结

相比传统的 Stack 架构,Fiber 将工作划分为多个工作单元,每个工作单元在执行实现后根据剩余时间决定是否让出控制权给浏览器执行渲染。并且它设置每个工作单元的优先级,暂停、重用和停止工作单元。每个 Fiber 节点都是 fiber tree 上的一个节点,通过子、兄弟和返回援用连贯,造成一个残缺的 fiber tree。

正文完
 0