乐趣区

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

一,概述

在 React 16 之前,VirtualDOM 的更新过程是采纳 Stack 架构实现的,也就是循环递归形式。这种比照形式有一个问题,就是一旦工作开始进行就无奈中断,如果利用中组件数量宏大,Virtual DOM 的层级就会比拟深。如果主线程被长期占用,就会阻塞渲染,造成卡顿。为了防止这种状况,须要执行更新操作时不能超过 16ms,如果超过 16ms,就须要先暂停,让给浏览器进行渲染操作,后续再继续执行更新计算。

而 Fiber 架构就是为了反对“可中断渲染”而创立的。在 React 中,fiber tree 是一种数据结构,它能够把虚构 dom tree 转换成一个链表,从而能够在执行遍历操作时反对断点重启,示意图如下。

二、Fiber 原理

Fiber 能够了解为是一个执行单元,也能够了解为是一种数据结构。

2.1 一个执行单元

Fiber 能够了解为一个执行单元,每次执行完一个执行单元,react 就会查看当初还剩多少工夫,如果没有工夫则将控制权让进来。React Fiber 与浏览器的外围交互流程如下图:

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

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

2.2 一种数据结构

在官网的文档介绍中,Fiber 被解释为一种数据结构,即咱们熟知的链表。每个 Virtual DOM 都能够示意为一个 fiber,如下图所示,每个节点都是一个 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 工作优先级)等等就不在过多的介绍了。

三、Fiber 执行流程

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

3.1 render 阶段

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

3.1.1 遍历流程

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

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

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 收集结束,完结。

收集 effect list 的遍历程序示意图如下:

3.2 commit 阶段

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

3.2.1 依据 effect list 更新视图

此阶段,依据一个 fiber 的 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
}

/**
* 依据一个 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})
}

四、总结

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

退出移动版