什么是Fiber
Fiber是一种架构,一个类似纤程(协程)的概念,一个执行单元.
他最大的作用就是保证了React的渲染是可中断可恢复的,不会长时间占用浏览器资源导致出现明显卡顿.
在ReactV16前,React经常会出现比较严重的卡帧问题,这是因为在V16版本前React的底层更新方法Reconciliation的更新思路是递归调用,而递归想要中断和回复是非常困难的.为了解决这个问题Meta当时选择的方法就是Diff和Fiber.
为什么需要Fiber
众所周知,JavaScript是单线程运行的,并且它跟浏览器的渲染线程是互斥的,只要js在运行,渲染就没法更新,所以如果js这边长时间占用线程就会导致页面"卡死",这样的用户体验就会很差.
- 对于前端来说,解决这个问题有三个方向,
-
- 优化任务,尽可能的快
- 让用户感觉很快,不阻塞运行
- 使用用WebWorker多线程运行
Vue选择的是第一种,React是第二种,至于第三种因为太过麻烦了,所以目前还没人这么干
如何实现Fiber
- Fiber的两大特点,可中断可恢复,并且可以主动让出浏览器资源
-
-
可中断可恢复
使用链表来讲VDOM树进行连接,方便
Reconciliation界面可以随时中止和恢复 -
主动让出机制
一般情况下,线程的调度机制分为两种
合作式调度以及抢占式调度,一般情况下抢占式调度会比较多,但是如果要满足保证页面流畅这个需求,那就得靠合作式调度
-
主动让出机制
如果要实现主动让出机制,那就得知道浏览器什么时候有空,有多少时间能够让我们执行任务
那么有没有这种API能满足我们的需求呢?
有的,requestIdleCallback API,能够满足超时检查的机制来让出控制权这个功能需求
window.requestIdleCallback(
callback: (dealine: IdleDeadline) => void,
option?: {timeout: number}
)
interface IdleDealine {
didTimeout: boolean // 表示任务执行是否超过约定时间
timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}
这个Api的效果就是,浏览器有空的时候执行我们的callback,然后告诉我们有多少时间,我们超过这个时间就把控制权让回去.
- 浏览器的刷新是按帧刷新,一般情况下是一秒六十帧,每一帧完成它的固定任务之后剩余的就是空闲时间,而它需要完成的任务有以下几个
-
- 用户输入事件
- js执行
- requestAnimation调用
- 布局 Layout
- 绘制 Paint
- 当然,既然是空闲时间执行,那就有可能出现被"饿死"的情况,所以在这个Api的第二个参数可以指定一个超时时间,这样就会持续执行,另外引用一下大佬的一段话:
- 另外不建议在
requestIdleCallback中进行DOM操作,因为这可能导致样式重新计算或重新布局(比如操作DOM后马上调用getBoundingClientRect),这些时间很难预估的,很有可能导致回调执行超时,从而掉帧。 - 不过,目前这个API只有Chrome支持, 所以React自己实现了一个,之后会详细看看
- 上面说了,为了避免任务被饿死,可以设置一个超时时间. 这个超时时间不是死的,低优先级的可以慢慢等待, 高优先级的任务应该率先被执行. 目前 React 预定义了 5 个优先级, 这个我在[《谈谈React事件机制和未来(react-events)》]中也介绍过: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
-
Immediate(-1) - 这个优先级的任务会同步执行, 或者说要马上执行且不能中断 -
UserBlocking(250ms) 这些任务一般是用户交互的结果, 需要即时得到反馈 -
Normal(5s) 应对哪些不需要立即感受到的任务,例如网络请求 -
Low(10s) 这些任务可以放后,但是最终应该得到执行. 例如分析通知 -
Idle(没有超时时间) 一些没有必要做的任务 (e.g. 比如隐藏的内容), 可能会被饿死
Fiber的内容
- Fiber的内容相比递归时期的架构要复杂了很多,不那么好理解,不过Fiber对性能和体验带来的提升太大了,还是很值得一学的,我们先来简单的看下Fiber里面有什么东西
-
interface Fiber { /** * ⚛️ 节点的类型信息 */ // 标记 Fiber 类型, 例如函数组件、类组件、宿主组件 tag: WorkTag, // 节点元素类型, 是具体的类组件、函数组件、宿主组件(字符串) type: any, /** * ⚛️ 结构信息 */ return: Fiber | null, child: Fiber | null, sibling: Fiber | null, // 子节点的唯一键, 即我们渲染列表传入的key属性 key: null | string, /** * ⚛️ 节点的状态 */ // 节点实例(状态): // 对于宿主组件,这里保存宿主组件的实例, 例如DOM节点。 // 对于类组件来说,这里保存类组件的实例 // 对于函数组件说,这里为空,因为函数组件没有实例 stateNode: any, // 新的、待处理的props pendingProps: any, // 上一次渲染的props memoizedProps: any, // The props used to create the output. // 上一次渲染的组件状态 memoizedState: any, /** * ⚛️ 副作用 */ // 当前节点的副作用类型,例如节点更新、删除、移动 effectTag: SideEffectTag, // 和节点关系一样,React 同样使用链表来将所有有副作用的Fiber连接起来 nextEffect: Fiber | null, /** * ⚛️ 替身 * 指向旧树中的节点 */ alternate: Fiber | null, }
双缓冲机制
WIP 树构建这种技术类似于图形化领域的'双缓存(Double Buffering) '技术, 图形绘制引擎一般会使用双缓冲技术,先将图片绘制到一个缓冲区,再一次性传递给屏幕进行显示,这样可以防止屏幕抖动,优化渲染性能。
放到React 中,WIP树就是一个缓冲,它在Reconciliation 完毕后一次性提交给浏览器进行渲染。它可以减少内存分配和垃圾回收,WIP 的节点不完全是新的,比如某颗子树不需要变动,React会克隆复用旧树中的子树。
双缓存技术还有另外一个重要的场景就是异常的处理,比如当一个节点抛出异常,仍然可以继续沿用旧树的节点,避免整棵树挂掉。
Dan 在 Beyond React 16 演讲中用了一个非常恰当的比喻,那就是Git 功能分支,你可以将 WIP 树想象成从旧树中 Fork 出来的功能分支,你在这新分支中添加或移除特性,即使是操作失误也不会影响旧的分支。当你这个分支经过了测试和完善,就可以合并到旧分支,将其替换掉. 这或许就是’提交(commit)阶段‘的提交一词的来源吧? :
为什么 Vue 中不需要使用 Fiber
其实这个问题也可以叫做:为什么 Vue 不需要时间分片?对于这个问题其实尤雨溪也在英文社区里回答过,也有前端大牛翻译发布在公众号上,那么下面我也进行一下总结。
第一,首先时间分片是为了解决 CPU 进行大量计算的问题,因为 React 本身架构的问题,在默认的情况下更新会进行过多的计算,就算使用 React 提供的性能优化 API,进行设置,也会因为开发者本身的问题,依然可能存在过多计算的问题。
第二,而 Vue 通过响应式依赖跟踪,在默认的情况下可以做到只进行组件树级别的更新计算,而默认下 React 是做不到的(据说 React 已经在进行这方面的优化工作了),再者 Vue 是通过 template 进行编译的,可以在编译的时候进行非常好的性能优化,比如对静态节点进行静态节点提升的优化处理,而通过 JSX 进行编译的 React 是做不到的。
第三,React 为了解决更新的时候进行过多计算的问题引入了时间分片,但同时又带来了额外的计算开销,就是任务协调的计算,虽然 React 也使用最小堆等的算法进行优化,但相对 Vue 还是多了额外的性能开销,因为 Vue 没有时间分片,所以没有这方面的性能担忧。
第四,根据研究表明,人类的肉眼对 100 毫秒以内的时间并不敏感,所以时间分片只对于处理超过 100 毫秒以上的计算才有很好的收益,而 Vue 的更新计算是很少出现 100 毫秒以上的计算的,所以 Vue 引入时间分片的收益并不划算。
总结
Fiber是React官方为了解决原先递归结构容易造成页面卡死而开发的新架构,主要是通过将原先的Reconciliation分成多个小的Diff比对来实现尽可能的无卡顿刷新,而其中的关键就是这个Fiber架构,通过链表的形式来实现原来递归做的事情,虽然代码复杂度上去了,但是用户体验的提升是非常大的,Diff是两个VDOM树比对更新的算法,而Fiber则是VDOM树目前实际的内容节点.
Diff算法
为什么会需要Diff算法
react和vue开发的页面都是SPA,然后为了每次页面的刷新都依赖语言自身去维护,如果每次都整个页面重渲染的话页面性能就会很差,所以双方都建立了自己的VDOM树,而为什么每次更新只做必要的更新,React这边对VDOM树的遍历做了个算法游戏,这个算法就是Diff算法.
内容
React的VDOM是一个链表,并且是单向的,为了保证能更新到最新的子级,Diff算法是深度优先,并且是从左到右.
深度优先,有子节点,就遍历子节点,没有子节点,就找兄弟节点,没有兄弟节点,就找叔叔节点,叔叔节点也没有的话,就继续往上找,它爷爷的兄弟,如果一直没找到,就代表所有的更新任务都更新完毕了
如果是初始渲染,那么协调位置就只是记录当前元素下标的位置到 Fiber 节点上。如果是更新阶段,就先判断有没有老 Fiber 节点,如果没有老 Fiber 节点,则说明该节点需要创建,就给当前新的 Fiber 节点打上一个 Placement 的标记,如果有老 Fiber 节点,则判断老 Fiber 节点的位置是否比上一次协调的返回的位置小,如果是,则说明该节点需要移动,给新 Fiber 节点打上一个 Placement 的标记,并继续返回上一次协调返回的位置;如果老 Fiber 节点的位置大或者等于上一次协调返回的位置,则说明该节点不需要进行位置移动操作,就返回老 Fiber 的位置即可。
总个来说,React Diff 算法分以下几个步骤:
-
第一轮,从左向右新老节点进行比对查找能复用的旧节点,如果有新老节点比对不成功的,则停止这一轮的比对,并记录了停止的位置。
-
如果第一轮比对,能把所有的新节点都比对完毕,则删除旧节点还没进行比对的节点。
-
如果第一轮的比对,没能将所有的新节点都比对完毕,则继续从第一轮比对停止的位置继续开始循环新节点,拿每一个新节点去老节点里面进行查找,有匹配成功的则复用,没匹配成功的则在协调位置的时候打上 Placement 的标记。
-
在所有新节点比对完毕之后,检查还有没有没进行复用的旧节点,如果有,则全部删除。
作者:Cobyte 链接:juejin.cn/post/711614… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
React和Vue的Diff的不同点
vue对静态节点可以单独特殊处理,不需要比对,这是得益于他的template写法,可以把静态节点提升处理,diff时不进循环.
总结
Diff是为了VDOM比较时能尽快的解决遍历结束而研发的算法,Vue和React都有实现不同的Diff算法类型,但是两者在部分核心内容上都有不少相似.
DIff方面相关知识参考以下文章:
作者:Cobyte 链接:juejin.cn/post/711614… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。