Description
React Fiber原理
什么是React Fiber
Fiber是React16中的协调引擎,主要目的是使Virtual DOM可以进行增量式渲染:能够将渲染工作分割成块,并将其分散到过个帧中
React概念
reconciliation
和解,React算法,用来比较两棵树,来确定哪些部分需要被改变。
当渲染React应用程序时,会生成一个描述应用程序的节点树并保存在内存中。然后将该树刷新到渲染环境 - 例如,在浏览器中,将其转换为一组DOM操作。当应用程序更新(通常通过setState),生成一个新的树。新树与前一棵树有区别,以计算需要更新呈现的应用程序的操作。
reconciliation的关键点:
- 假定不同的组件类型产生实质上不同的树。React不会试图区分它们,而是完全替换旧的树。
- 列表的区分使用keys来执行。关键应该是“稳定,可预测,独特”。
React将reconciliation和渲染分开进行设计,reconciliation用来计算哪些节点需要被改变,渲染过程将这些变动通过不同的渲染器进行呈现(例如React Native和React DOM)
Fiber将重新实现reconciliation,使它变的更加的高效
目前的问题:
更新过程是同步的,没有很好的利用scheduling,当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,就会一直进行到底
在用户界面上,很多更新其实没有必要立即应用,允许一定时间的延迟。应用每个更新会造成资源浪费,导致丢帧等问题,因为浏览器需要花更多的精力来计算这些变动,导致界面的渲染受到影响
当界面组件树比较小时,不会有什么影响,但当组件树过于庞大时,浏览器可能会对input之类的事件及时响应
Fiber架构的目的
Fiber的主要目标是使React能够利用scheduling。具体来说,需要能够
- 暂停工作,稍后再回来。
- 为不同类型的工作分配优先权。
- 重复以前完成的工作。
- 如果不再需要,请中止工作。
为了做到这一点,首先需要一种把工作分解成单元的方法。从某种意义上说,这就是fiber。fiber代表一个工作单元。
计算机跟踪程序执行的方式通常是使用调用堆栈,当函数被执行时,相关信息会被push进堆栈中
在处理UI时,问题是如果一次执行了太多的工作,它可能会导致动画丢帧,看起来不稳定。更重要的是,如果某些工作被更新的更新所取代,那么这些工作可能是不必要的。这是UI组件和函数之间的比较失败的地方,因为组件比一般的函数具有更多特定的问题。
而Fiber重新实现堆栈,专门用于React组件。可以将单个fiber视为虚拟堆栈帧。
这使得开发人员可以自己控制堆栈中内容的执行,而不是完全依赖于计算机
Fiber的原理
React Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。
维护每一个分片的数据结构,就是Fiber。
因此,需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
- synchronous,与之前的Stack Reconciler操作一样,同步执行
- task,在next tick之前执行
- animation,下一帧之前执行
- high,在不久的将来立即执行
- low,稍微延迟执行也没关系
- offscreen,下一次render时或scroll时才执行
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
并且,Fiber Reconciler将执行过程分为两个阶段
- Reconciliation Phase,生成Fiber树,得到需要更新的节点,此过程可以被打断,分多步进行,这些生命周期会在第一阶段调用
- componentWillMount
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- Commit Phase,将需要更新的节点批量更新,此过程不能被打断,这些生命周期会在第二阶段调用
- componentDidMount
- componentDidUpdate
- componentWillUnmount
在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!
Fiber树
Fiber之前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update
,无法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响体验
Fiber解决这个问题的思路是把渲染/更新过程(递归diff)拆分成一系列小任务,每次检查树上的一小部分,做完看是否还有时间继续下一个任务,有的话继续,没有的话把自己挂起,主线程不忙的时候再继续
增量更新需要更多的上下文信息,之前的vDOM tree显然难以满足,所以扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree
Fiber其实是一种数据结构,可以用一个纯JS对象来表示
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
Fiber树的更新
具体过程如下(以组件节点为例):
- 如果当前节点不需要更新,直接把子节点clone过来,跳到5;要更新的话打个tag
- 更新当前节点状态(
props, state, context
等) - 调用
shouldComponentUpdate()
,false
的话,跳到5 - 调用
render()
获得新的子节点,并为子节点创建fiber(创建过程会尽量复用现有fiber,子节点增删也发生在这里) - 如果没有产生child fiber,该工作单元结束,把effect list归并到return,并把当前节点的sibling作为下一个工作单元;否则把child作为下一个工作单元
- 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
- 如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态
使用的浏览器的API
Fiber的实现方式是使用了浏览器的requestIdleCallback
这一 API
此API的解释如下:
window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。
总结
从底层思路上来说,Fiber将原来计算diff更新的过程由递归改成了循环,使得操作能被中断,将任务分片,提高浏览器性能