详解React Fiber架构原理

目录
  • 一、概述
  • 二、Fiber架构
    • 2.1 执行单元
    • 2.2 数据结构
    • 2.3 Fiber链表结构
    • 2.4 Fiber节点
    • 2.5 API
      • 2.5.1 requestAnimationFrame
      • 2.5.2 requestIdleCallback
  • 三、Fiber执行流程
    • 3.1 render阶段
      • 3.1.1 遍历流程
      • 3.1.2 收集effect list
    • 3.2 commit阶段
      • 3.2.1 根据effect list 更新视图
      • 3.2.2 视图更新
  • 四、总结

一、概述

在 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。

到此这篇关于React Fiber架构原理剖析的文章就介绍到这了,更多相关React Fiber原理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解React Fiber的工作原理

    啥是React Fiber? React Fiber,简单来说就是一个从React v16开始引入的新协调引擎,用来实现Virtual DOM的增量渲染. 说人话:就是一种能让React视图更新过程变得更加流畅顺滑的处理手法. 我们都知道:进程大,线程小.而Fiber(纤维)是一种比线程还要细粒度的处理机制.从这个单词也可以猜测:React Fiber会很"细".到底怎么个细法,我们接着往下看. 为什么会有React Fiber? 之前说了,React Fiber是为了让React的视

  • React Fiber结构的创建步骤

    React Fiber的创建 当前React版本基于V17.0.2版本,本篇主要介绍fiber结构的创建. 一.开始之前 个人理解,如有不对,请指出. 首先需要配置好React的debugger开发环境,入口在这里:github 执行npm run i,安装依赖,npm start运行环境. 二.从React.render开始 通过在项目入口处调用React.render,打上Debug,查看React调用栈. const root = document.getElementById('root

  • 详解React Fiber架构原理

    目录 一.概述 二.Fiber架构 2.1 执行单元 2.2 数据结构 2.3 Fiber链表结构 2.4 Fiber节点 2.5 API 2.5.1 requestAnimationFrame 2.5.2 requestIdleCallback 三.Fiber执行流程 3.1 render阶段 3.1.1 遍历流程 3.1.2 收集effect list 3.2 commit阶段 3.2.1 根据effect list 更新视图 3.2.2 视图更新 四.总结 一.概述 在 React 16

  • 详解React setState数据更新机制

    为什么使用setState 在React 的开发过程中,难免会与组件的state打交道.使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法.为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个demo. class Index extends React.Component { this.state = { count: 0 } onClick = () => { this.setState({ count: 10 }) } re

  • 详解React中Props的浅对比

    上一周去面试的时候,面试官我PureComponent里是如何对比props的,概念已经牢记脑中,脱口而出就是浅对比,接着面试官问我是如何浅对比的,结果我就没回答上来. 趁着周末,再来看看源码里是如何实现的. 类组件的Props对比 类组件是否需要更新需要实现shouldComponentUpdate方法,通常讲的是如果继承的是PureComponent则会有一个默认浅对比的实现. // ReactBaseClasses.js function ComponentDummy() {} Compo

  • 详解React Angular Vue三大前端技术

    一.[React] React(也被称为React.js或ReactJS)是一个用于构建用户界面的JavaScript库.它由Facebook和一个由个人开发者和公司组成的社区来维护. React可以作为开发单页或移动应用的基础.然而,React只关注向DOM渲染数据,因此创建React应用通常需要使用额外的库来进行状态管理和路由,Redux和React Router分别是这类库的例子. 基本用法 下面是一个简单的React在HTML中使用JSX和JavaScript的例子. Greeter函数

  • 详解React中的不可变值

    什么是不可变值 函数式编程是指程序里面的函数和表达式都能像数学中的函数一样,给定了输入值,输出是确定的.比如 let a = 1; let b = a + 1; => a = 1 b = 2; 变量b出现,虽然使用了变量a的值,但是没有修改a的值. 再看我们熟悉的react中的代码,假如初始化了this.state = { count: 1 } componentDidMount() { const newState = { ...state, count: 2 }; // { count: 2

  • 详解react setState

    setState是同步还是异步 自定义合成事件和react钩子函数中异步更新state 以在自定义click事件中的setState为例 import React, { Component } from 'react'; class Test extends Component { constructor(props) { super(props); this.state = { count: 1 }; } handleClick = () => { this.setState({ count:

  • 详解react应用中的DOM DIFF算法

    前言 对我们搞前端的来说,目前最流行的两大前端框架毫无疑问当属React和Vue,对于这两大框架,想必大家也是再熟悉不过了.然而,这两大框架无一例外的全部放弃使用传统的DOM技术,却采用了以JS为基础的Virtual DOM技术,也可称作虚拟DOM.所以,到底什么是Virtual DOM?两大热门框架全部使用Virtual DOM的原因又是什么?接下来让我这个搞前端的人来好好地为您讲解一下DOM DIFF算法的牛逼之处. 什么是Virtual DOM? 如字面意思所说,Virtual DOM即

  • 详解React Hooks是如何工作的

    1. React Hooks VS 纯函数 React Hook 说白了就是 React V18.6 新增的一些 API,API的本质就是提供某种功能的函数接口.因此,React Hooks 就是一些函数,但是 React Hooks 不是纯函数. 什么是纯函数呢?就是此函数在相同的输入值时,需产生相同的输出,并且此函数不能影响到外面的数据. 简单理解就是函数里面不能用到在外面定义的变量,因为如果用到了外面定义的变量,当外面的变量改变时会影响函数内部的计算,函数也会影响到外面的变量. 对于 Re

  • 详解PHP的执行原理和流程

    简介 先看看下面这个过程: • 我们从未手动开启过PHP的相关进程,它是随着Apache的启动而运行的: • PHP通过mod_php5.so模块和Apache相连(具体说来是SAPI,即服务器应用程序编程接口): • PHP总共有三个模块:内核.Zend引擎.以及扩展层: • PHP内核用来处理请求.文件流.错误处理等相关操作: • Zend引擎(ZE)用以将源文件转换成机器语言,然后在虚拟机上运行它: • 扩展层是一组函数.类库和流,PHP使用它们来执行一些特定的操作.比如,我们需要mysq

随机推荐