一,概述
在 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。