在讲Fiber之前先来了解下Fiber的前生Stack Reconciler
调和过程和diff算法
调和
又译为协调
,协调过程的官方定义如下:
Virtual DOM是一种编程概念,这个概念里UI以一种理想化的或者说虚拟的表现形式被保存在内存中,并通过ReactDOM等类库使之与真实的DOM同步,这一过程叫做协调(调和)。
通俗点说就是将虚拟DOM映射到真实DOM的过程,因此严格来说,调和过程不能和Diff画等号,调和是“使一致”的过程,而Diff是“找不同”的过程,他只是“使一致”过程中的一个环节。
但是在如今大众的认知里,当我们讨论调和时其实就是在讨论Diff,其实这样的认知也有其合理性,因为Diff确实是调和过程中最具代表性的一环,根据Diff实现的形式不同,调和过程被划分为以React 15为代表的“栈调和”以及React 16以来的“Fiber调和”。
Diff策略的设计思想
传统的计算方法是通过循环递归进行树节点的一一对比,这个过程算法复杂度是O(n^3),后来React团队结合设计层面的一些推导,将复杂度转换为O(n),下面是转换的前提:
- 若两个组件属于同一类型,那么他们将拥有相同的DOM树形结构
- 处于同一层级的一组节点,可以通过设置key作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性。
- DOM节点之间的跨层级操作并不多,同层级操作是主流
Diff算法的关键点
对于Diff算法我们只需要真正把握以下三点:
-
Diff算法性能突破的关键点在于分层对比
结合DOM节点之间跨层级操作并不多,同层级操作是主流这一规律,React的Diff算法直接放弃了跨层级的节点比较,他只针对相同层级的节点做对比。
-
类型一致的节点才有继续Diff的必要性
结合“若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树形结构”这一规律,我们虽不能直接反推出“不同类型的组件 DOM 结构不同”,但在大部分的情况下,这个结论都是成立的。毕竟,实际开发中遇到两个 DOM 结构完全一致、而类型不一致的组件的概率确实太低了。
-
key属性的设置,可以帮我们尽可能的重用同一层内的节点
key
它试图解决的是同一层级下节点的复用问题,在官网上是这样定义key的:key 是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,React 则会触发 UI 的重渲染。这是一个非常有用的特性。
既然Diff算法已经能达到这样好的效果,为什么React团队还要大费周章的去开发Fiber架构?首先我们从
Stack Reconciler
带来的问题作为切入点。前置知识——单线程js和多线程浏览器
我们都知道,javascript是单线程的,浏览器是多线程的,对于多线程的浏览器来说,它除了要处理js线程之外,还需要处理包括事件系统、定时器、网络请求以及负责DOM的UI渲染线程。
重要的来了javascript线程是可以操作DOM的,这意味着我们的渲染线程和js线程不能同时工作,当一个在执行时另一个必须挂起。在这种机制下,如果js线程长时间占用主线程,那么渲染层面的更新就不得不长时间的等待,界面长时间不更新带给用户的体验就是“卡顿”为什么会产生“卡顿”这样的局面
Stack Reconciler 所带来的一个无解的问题,正是JavaScript对主线程的超时占用问题。为什么会出现这个问题?因为Stack Reconciler是一个同步的递归过程。
同步的递归过程意味着一旦更新开始,就根本停不下来,从本质上来说,栈调和机制下的Diff算法其实是树的深度优先遍历过程,这个过程的致命性在于它是同步的,不可以被打断的。当处理结构相对复杂、体量相对庞大的虚拟DOM树时,Stack Reconciler需要的调和时间会很长,这就意味着js线程将长时间的霸占主线程,进而导致我们上文所描述的渲染卡顿。
Fiber是如何解决问题的
Fiber从字面意思来理解就是“丝、纤维”的意思,在计算机科学里我们有线程、进程之分,而Fiber就是比线程还要纤细的一个过程,也就是所谓的纤程,纤程的出现意在对渲染过程实现更加精细的控制。
下面是官网的解答Fiber架构的应用目的,按照React官方的说法,是实现增量渲染,所谓“增量渲染”通俗来说就是把一个渲染任务分解成多个渲染任务,而后将其分散到多个帧里面,不过严格来说,增量渲染其实是一种手段,实现增量渲染的目的是为了实现任务的可终中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用户体验。
下面通过图来对比一下:
在React16之前,React的渲染和更新阶段依赖的是两层架构
正如上文所分析的那样,Reconciler 这一层负责对比出新老虚拟 DOM 之间的变化,Renderer 这一层负责将变化的部分应用到视图上,从 Reconciler 到 Renderer 这个过程是严格同步的。
而在 React 16 中,为了实现“可中断”和“优先级”,两层架构变成了如下图所示的三层架构:
多出来的这层架构叫做“Scheduler(调度器)”,调度器的作用是调度更新的优先级。
在这套架构模式下,更新的处理工作流变成了这样:首先每个更新任务都会被赋予优先级,当更新任务到达调度器时,高优先级的任务会更快地被调度进Reconciler层,此时如果有新的更新任务抵达调度器,调度器会先检查他的优先级,若发现他的优先级大于当前任务的优先级,那么就会将处于reconciler层的任务
中断
,调度器会将新的任务推进Reconciler,当新任务执行完之后,之前被中断的任务会重新被推进Reconciler层,继续它的渲染之旅,这便是所谓的可恢复
。Fiber架构对生命周期的影响
React16的生命周期分为三个阶段:
- render阶段:纯净且没有副作用,可能会被React暂停、终止或重新启动。
- pre-commit阶段:可以读取DOM
- commit阶段:可以使用DOM,运行副作用,安排更新
在render阶段,React主要是在内存中做计算,明确DOM树的更新点,而commit阶段则负责把render阶段生成的更新真正执行掉,首先我们来看React 15中从render到commit的过程:
在React 16中,render到commit的过程变成这样:
可以看出,新老架构对React生命周期的影响主要在render阶段,在render阶段,一个庞大的更新任务被分解成一个个工作单元,这些工作单元有着不同的优先级,React可以根据优先级的高低去实现工作单元的打断和恢复。