一.目标
Fiber是对React核心算法的重构,2年重构的产物就是Fiber reconcilernode
核心目标:扩大其适用性,包括动画,布局和手势,包括5个具体目标(后2个算送的):react
把可中断的工做拆分红小任务算法
对正在作的工做调整优先次序、重作、复用上次(作了一半的)成果redux
在父子任务之间从容切换(yield back and forth),以支持React执行过程当中的布局刷新react-native
支持render()返回多个元素浏览器
更好地支持error boundary网络
既然初衷是不但愿JS不受控制地长时间执行(想要手动调度),那么,为何JS长时间执行会影响交互响应、动画?并发
由于JavaScript在浏览器的主线程上运行,刚好与样式计算、布局以及许多状况下的绘制一块儿运行。若是JavaScript运行时间过长,就会阻塞这些其余工做,可能致使掉帧。app
(引自Optimize JavaScript Execution)less
React但愿经过Fiber重构来改变这种不可控的现状,进一步提高交互体验
P.S.关于Fiber目标的更多信息,请查看Codebase Overview
二.关键特性
Fiber的关键特性以下:
增量渲染(把渲染任务拆分红块,匀到多帧)
更新时可以暂停,终止,复用渲染任务
给不一样类型的更新赋予优先级
并发方面新的基础能力
增量渲染用来解决掉帧的问题,渲染任务拆分以后,每次只作一小段,作完一段就把时间控制权交还给主线程,而不像以前长时间占用。这种策略叫作cooperative scheduling(合做式调度),操做系统的3种任务调度策略之一(Firefox还对真实DOM应用了这项技术)
另外,React自身的killer feature是virtual DOM,2个缘由:
coding UI变简单了(不用关心浏览器应该怎么作,而是把下一刻的UI描述给React听)
既然DOM能virtual,别的(硬件、VR、native App)也能
React实现上分为2部分:
reconciler 寻找某时刻先后两版UI的差别。包括以前的Stack reconciler与如今的Fiber reconciler
renderer 插件式的,平台相关的部分。包括React DOM、React Native、React ART、ReactHardware、ReactAframe、React-pdf、ReactThreeRenderer、ReactBlessed等等
这一波是对reconciler的完全改造,对killer feature的加强
三.fiber与fiber tree
React运行时存在3种实例:
DOM 真实DOM节点 ------- Instances React维护的vDOM tree node ------- Elements 描述UI长什么样子(type, props)
Instances是根据Elements建立的,对组件及DOM节点的抽象表示,vDOM tree维护了组件状态以及组件与DOM树的关系
在首次渲染过程当中构建出vDOM tree,后续须要更新时(setState()),diff vDOM tree获得DOM change,并把DOM change应用(patch)到DOM树
Fiber以前的reconciler(被称为Stack reconciler)自顶向下的递归mount/update,没法中断(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就没法当即获得处理,影响体验
Fiber解决这个问题的思路是把渲染/更新过程(递归diff)拆分红一系列小任务,每次检查树上的一小部分,作完看是否还有时间继续下一个任务,有的话继续,没有的话把本身挂起,主线程不忙的时候再继续
增量更新须要更多的上下文信息,以前的vDOM tree显然难以知足,因此扩展出了fiber tree(即Fiber上下文的vDOM tree),更新过程就是根据输入数据以及现有的fiber tree构造出新的fiber tree(workInProgress tree)。所以,Instance层新增了这些实例:
DOM 真实DOM节点 ------- effect 每一个workInProgress tree节点上都有一个effect list 用来存放diff结果 当前节点更新完毕会向上merge effect list(queue收集diff结果) - - - - workInProgress workInProgress tree是reconcile过程当中从fiber tree创建的当前进度快照,用于断点恢复 - - - - fiber fiber tree与vDOM tree相似,用来描述增量更新所需的上下文信息 ------- Elements 描述UI长什么样子(type, props)
注意:放在虚线上的2层都是临时的结构,仅在更新时有用,平常不持续维护。effect指的就是side effect,包括将要作的DOM change
fiber tree上各节点的主要结构(每一个节点称为fiber)以下:
// fiber tree节点结构 { stateNode, child, return, sibling, ... }
return表示当前节点处理完毕后,应该向谁提交本身的成果(effect list)
P.S.fiber tree其实是个单链表(Singly Linked List)树结构,见react/packages/react-reconciler/src/ReactFiber.js
P.S.注意小fiber与大Fiber,前者表示fiber tree上的节点,后者表示React Fiber
四.Fiber reconciler
reconcile过程分为2个阶段(phase):
(可中断)render/reconciliation 经过构造workInProgress tree得出change
(不可中断)commit 应用这些DOM change
render/reconciliation
以fiber tree为蓝本,把每一个fiber做为一个工做单元,自顶向下逐节点构造workInProgress tree(构建中的新fiber tree)
具体过程以下(以组件节点为例):
若是当前节点不须要更新,直接把子节点clone过来,跳到5;要更新的话打个tag
更新当前节点状态(props, state, context等)
调用shouldComponentUpdate(),false的话,跳到5
调用render()得到新的子节点,并为子节点建立fiber(建立过程会尽可能复用现有fiber,子节点增删也发生在这里)
若是没有产生child fiber,该工做单元结束,把effect list归并到return,并把当前节点的sibling做为下一个工做单元;不然把child做为下一个工做单元
若是没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工做单元;不然,当即开始作
若是没有下一个工做单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态
其实是1-6的工做循环,7是出口,工做循环每次只作一件事,作完看要不要喘口气。工做循环结束时,workInProgress tree的根节点身上的effect list就是收集到的全部side effect(由于每作完一个都向上归并)
因此,构建workInProgress tree的过程就是diff的过程,经过requestIdleCallback来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次requestIdleCallback回调再继续构建workInProgress tree
P.S.Fiber以前的reconciler被称为Stack reconciler,就是由于这些调度上下文信息是由系统栈来保存的。虽然以前一次性作完,强调栈没什么意义,起个名字只是为了便于区分Fiber reconciler
requestIdleCallback
通知主线程,要求在不忙的时候告诉我,我有几个不太着急的事情要作
具体用法以下:
window.requestIdleCallback(callback[, options]) // 示例 let handle = window.requestIdleCallback((idleDeadline) => { const {didTimeout, timeRemaining} = idleDeadline; console.log(`超时了吗?${didTimeout}`); console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`); // do some stuff const now = +new Date, timespent = 10; while (+new Date < now + timespent); console.log(`花了${timespent}ms搞事情`); console.log(`可用时间剩余${timeRemaining.call(idleDeadline)}ms`); }, {timeout: 1000}); // 输出结果 // 超时了吗?false // 可用时间剩余49.535000000000004ms // 花了10ms搞事情 // 可用时间剩余38.64ms
注意,requestIdleCallback调度只是但愿作到流畅体验,并不能绝对保证什么,例如:
// do some stuff const now = +new Date, timespent = 300; while (+new Date < now + timespent);
若是搞事情(对应React中的生命周期函数等时间上不受React控制的东西)就花了300ms,什么机制也保证不了流畅
P.S.通常剩余可用时间也就10-50ms,可调度空间不很宽裕
commit
第2阶段直接一口气作完:
处理effect list(包括3种处理:更新DOM树、调用组件生命周期函数以及更新ref等内部状态)
出对结束,第2阶段结束,全部更新都commit到DOM树上了
注意,真的是一口气作完(同步执行,不能喊停)的,这个阶段的实际工做量是比较大的,因此尽可能不要在后3个生命周期函数里干重活儿
生命周期hook
生命周期函数也被分为2个阶段了:
// 第1阶段 render/reconciliation componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate // 第2阶段 commit componentDidMount componentDidUpdate componentWillUnmount
第1阶段的生命周期函数可能会被屡次调用,默认以low优先级(后面介绍的6种优先级之一)执行,被高优先级任务打断的话,稍后从新执行
五.fiber tree与workInProgress tree
双缓冲技术(double buffering),就像redux里的nextListeners,以fiber tree为主,workInProgress tree为辅
双缓冲具体指的是workInProgress tree构造完毕,获得的就是新的fiber tree,而后喜新厌旧(把current指针指向workInProgress tree,丢掉旧的fiber tree)就行了
这样作的好处:
可以复用内部对象(fiber)
节省内存分配、GC的时间开销
每一个fiber上都有个alternate属性,也指向一个fiber,建立workInProgress节点时优先取alternate,没有的话就建立一个:
let workInProgress = current.alternate; if (workInProgress === null) { //...这里颇有意思 workInProgress.alternate = current; current.alternate = workInProgress; } else { // We already have an alternate. // Reset the effect tag. workInProgress.effectTag = NoEffect; // The effect list is no longer valid. workInProgress.nextEffect = null; workInProgress.firstEffect = null; workInProgress.lastEffect = null; }
如注释指出的,fiber与workInProgress互相持有引用,“喜新厌旧”以后,旧fiber就做为新fiber更新的预留空间,达到复用fiber实例的目的
P.S.源码里还有一些有意思的技巧,好比tag的位运算
六.优先级策略
每一个工做单元运行时有6种优先级:
synchronous 与以前的Stack reconciler操做同样,同步执行
task 在next tick以前执行
animation 下一帧以前执行
high 在不久的未来当即执行
low 稍微延迟(100-200ms)执行也不要紧
offscreen 下一次render时或scroll时才执行
synchronous首屏(首次渲染)用,要求尽可能快,无论会不会阻塞UI线程。animation经过requestAnimationFrame来调度,这样在下一帧就能当即开始动画过程;后3个都是由requestIdleCallback回调执行的;offscreen指的是当前隐藏的、屏幕外的(看不见的)元素
高优先级的好比键盘输入(但愿当即获得反馈),低优先级的好比网络请求,让评论显示出来等等。另外,紧急的事件容许插队
这样的优先级机制存在2个问题:
生命周期函数怎么执行(可能被频频中断):触发顺序、次数没有保证了
starvation(低优先级饿死):若是高优先级任务不少,那么低优先级任务根本没机会执行(就饿死了)
生命周期函数的问题有一个官方例子:
low A componentWillUpdate() --- high B componentWillUpdate() componentDidUpdate() --- restart low A componentWillUpdate() componentDidUpdate()
第1个问题正在解决(还没解决),生命周期的问题会破坏一些现有App,给平滑升级带来困难,Fiber团队正在努力寻找优雅的升级途径
第2个问题经过尽可能复用已完成的操做(reusing work where it can)来缓解,听起来也是正在想办法解决
这两个问题自己不太好解决,只是解决到什么程度的问题。好比第一个问题,若是组件生命周期函数掺杂反作用太多,就没有办法无伤解决。这些问题虽然会给升级Fiber带来必定阻力,但毫不是不可解的(退一步讲,若是新特性有足够的吸引力,第一个问题你们本身想办法就解决了)
七.总结
已知
React在一些响应体验要求较高的场景不适用,好比动画,布局和手势
根本缘由是渲染/更新过程一旦开始没法中断,持续占用主线程,主线程忙于执行JS,无暇他顾(布局、动画),形成掉帧、延迟响应(甚至无响应)等不佳体验
求
一种可以完全解决主线程长时间占用问题的机制,不只可以应对眼前的问题,还要有长远意义
The “fiber” reconciler is a new effort aiming to resolve the problems inherent in the stack reconciler and fix a few long-standing issues.
解
把渲染/更新过程拆分为小块任务,经过合理的调度机制来控制时间(更细粒度、更强的控制力)
那么,面临5个子问题:
1.拆什么?什么不能拆?
把渲染/更新过程分为2个阶段(diff + patch):
1.diff ~ render/reconciliation
2.patch ~ commit
diff的实际工做是对比prevInstance和nextInstance的状态,找出差别及其对应的DOM change。diff本质上是一些计算(遍历、比较),是可拆分的(算一半待会儿接着算)
patch阶段把本次更新中的全部DOM change应用到DOM树,是一连串的DOM操做。这些DOM操做虽然看起来也能够拆分(按照change list一段一段作),但这样作一方面可能形成DOM实际状态与维护的内部状态不一致,另外还会影响体验。并且,通常场景下,DOM更新的耗时比起diff及生命周期函数耗时不算什么,拆分的意义不很大
因此,render/reconciliation阶段的工做(diff)能够拆分,commit阶段的工做(patch)不可拆分
P.S.diff与reconciliation只是对应关系,并不等价,若是非要区分的话,reconciliation包括diff:
This is a part of the process that React calls reconciliation which starts when you call ReactDOM.render() or setState(). By the end of the reconciliation, React knows the result DOM tree, and a renderer like react-dom or react-native applies the minimal set of changes necessary to update the DOM nodes (or the platform-specific views in case of React Native).
(引自Top-Down Reconciliation)
2.怎么拆?
先凭空乱来几种diff工做拆分方案:
按组件结构拆。很差分,没法预估各组件更新的工做量
按实际工序拆。好比分为getNextState(), shouldUpdate(), updateState(), checkChildren()再穿插一些生命周期函数
按组件拆太粗,显然对大组件不太公平。按工序拆太细,任务太多,频繁调度不划算。那么有没有合适的拆分单位?
有。Fiber的拆分单位是fiber(fiber tree上的一个节点),实际上就是按虚拟DOM节点拆,由于fiber tree是根据vDOM tree构造出来的,树结构如出一辙,只是节点携带的信息有差别
因此,其实是vDOM node粒度的拆分(以fiber为工做单元),每一个组件实例和每一个DOM节点抽象表示的实例都是一个工做单元。工做循环中,每次处理一个fiber,处理完能够中断/挂起整个工做循环
3.如何调度任务?
分2部分:
工做循环
优先级机制
工做循环是基本的任务调度机制,工做循环中每次处理一个任务(工做单元),处理完毕有一次喘息的机会:
// Flush asynchronous work until the deadline runs out of time. while (nextUnitOfWork !== null && !shouldYield()) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); }
shouldYield就是看时间用完了没(idleDeadline.timeRemaining()),没用完的话继续处理下一个任务,用完了就结束,把时间控制权还给主线程,等下一次requestIdleCallback回调再接着作:
// If there's work left over, schedule a new callback. if (nextFlushedExpirationTime !== NoWork) { scheduleCallbackWithExpiration(nextFlushedExpirationTime); }
也就是说,(不考虑突发事件的)正常调度是由工做循环来完成的,基本规则是:每一个工做单元结束检查是否还有时间作下一个,没时间了就先“挂起”
优先级机制用来处理突发事件与优化次序,例如:
到commit阶段了,提升优先级
高优任务作一半出错了,给降一下优先级
抽空关注一下低优任务,别给饿死了
若是对应DOM节点此刻不可见,给降到最低优先级
这些策略用来动态调整任务调度,是工做循环的辅助机制,最早作最重要的事情
4.如何中断/断点恢复?
中断:检查当前正在处理的工做单元,保存当前成果(firstEffect, lastEffect),修改tag标记一下,迅速收尾并再开一个requestIdleCallback,下次有机会再作
断点恢复:下次再处理到该工做单元时,看tag是被打断的任务,接着作未完成的部分或者重作
P.S.不管是时间用尽“天然”中断,仍是被高优任务粗暴打断,对中断机制来讲都同样
5.如何收集任务结果?
Fiber reconciliation的工做循环具体以下:
找到根节点优先级最高的workInProgress tree,取其待处理的节点(表明组件或DOM节点)
检查当前节点是否须要更新,不须要的话,直接到4
标记一下(打个tag),更新本身(组件更新props,context等,DOM节点记下DOM change),并为孩子生成workInProgress node
若是没有产生子节点,归并effect list(包含DOM change)到父级
把孩子或兄弟做为待处理节点,准备进入下一个工做循环。若是没有待处理节点(回到了workInProgress tree的根节点),工做循环结束
经过每一个节点更新结束时向上归并effect list来收集任务结果,reconciliation结束后,根节点的effect list里记录了包括DOM change在内的全部side effect
触类旁通
既然任务可拆分(只要最终获得完整effect list就行),那就容许并行执行(多个Fiber reconciler + 多个worker),首屏也更容易分块加载/渲染(vDOM森林)
并行渲染的话,听说Firefox测试结果显示,130ms的页面,只须要30ms就能搞定,因此在这方面是值得期待的,而React已经作好准备了,这也就是在React Fiber上下文常常听到的待unlock的更多特性之一
八.源码简析
从15到16,源码结构发生了很大变化:
再也看不到mountComponent/updateComponent()了,被拆分重组成了(beginWork/completeWork/commitWork())
ReactDOMComponent也被去掉了,在Fiber体系下DOM节点抽象用ReactDOMFiberComponent表示,组件用ReactFiberClassComponent表示,以前是ReactCompositeComponent
Fiber体系的核心机制是负责任务调度的ReactFiberScheduler,至关于以前的ReactReconciler
vDOM tree变成fiber tree了,之前是自上而下的简单树结构,如今是基于单链表的树结构,维护的节点关系更多一些
fiber tree来张图感觉一下:
fiber-tree
其实稍一细想,从Stack reconciler到Fiber reconciler,源码层面就是干了一件递归改循环的事情(固然,实际作的事情远不止递归改循环,但这是第一步)
总之,源码变化很大,若是对Fiber思路没有预先了解的话,看源码会比较艰难(看过React[15-]的源码的话,就更容易迷惑了)
P.S.这张清明流程图要正式退役了
参考资料
Lin Clark – A Cartoon Intro to Fiber – React Conf 2017:5星推荐,声音很好听,比Jing Chen好100倍
acdlite/react-fiber-architecture
Codebase Overview
A look inside React Fiber – how work will get done.:Fiber源码解读,小说体看着有点费劲