React Fiber架构

介绍

对于一些react语境下的术语不翻译。原文javascript

React Fiber是对React核心算法的从新实现。这是一个正在进行中的项目。到目前为止(指2016年),React团队已经对此进行了为期两年的研究和调研。html

React Fiber的目标是增长React对动画,布局和手势等领域的是适配性(suitability)。React Fiber的头等特性是incremental rendering(增量渲染)。何为增量渲染?那就是一种将rendering work切割成chunks,而后将其分红多个帧。java

其实比较关键的特性包括有暂停,中断和在更新流程中复用work;对各类不一样类型的更新任务赋予不一样的优先级(priority);为concurrency做铺垫。react

关于本文档

React Fiber引入了几种新颖的概念。这些概念每每不能仅仅靠经过查阅源码就能理解的。 这个文档纯粹是我我的跟进React Fiber项目实现细节的过程当中记下来的笔记。随着这种笔记记得愈来愈多,我意识到这些笔记可能对别人也会有用。git

我打算用最简单,通俗易懂的语言来描述和表达,避免一上来就给你展现各类术语的艰涩难懂的定义。我也尽量地给出一些比较重要的外部资源的连接。github

注意,我不是React团队的成员,个人话并不具有权威性。所以,这不是官方的权威文档。不过,我已经咨询过几个React团队的成员,以确保所表达内容的准确性。算法

这份文档会随时变动,由于我感受React Fiber项目在完成以前随时都会进行一些重大的重构。固然,我也会尝试记录它的设计的变动。任何关于该文档的优化和建议都是十分欢迎的。c#

个人目的是,在读完这边文档以后,你在跟进它已经实现的部分过程当中,对React Fiber会有足够的了解。甚至最后,你可以反过来为React社区作出你本身的贡献。浏览器

先决知识

在继续阅读前,我强烈建议你过目一下如下罗列出来的文章:性能优化

重温

请检查一下你是否已经阅读过“先决知识”章节。若是没有,建议你去阅读一下。在咱们深刻接触新东西以前,咱们不妨重温几个概念。

什么是reconciliation?

reconciliation

是一种react用来diff(比对)两颗节点(好比react element)树,从而决定哪一部分须要更新的一种算法。

update

致使React app从新渲染的数据更改。一般来讲,setState就会致使一个更新。从新渲染做为更新的一个结果而存在。

React API的中心思想是这样的:一次update等同于整个app的从新渲染。这种设计有助于开发者用声明式的思惟方式去理解app是如何高效地将状态转换的(A to B, B to C, C to A),而不是用命令式的思惟方式去操心期间的细节。

不过,每一次数据的改变致使整个app的从新渲染只会发生在一些不过重要app上。在真实的世界里,通常不会这么作。由于这么作会致使很昂贵的性能消耗。React已经在呈现最新界面的同时帮咱们作好了性能优化。大部分这些优化就是咱们所提reconciliation算法的一部分。

大众所熟知的"virtual DOM"的背后就是reconciliation算法。一个高水平的描述是这样的:当你渲染一个react app的时候,那么react就会生成一颗用于描述该app的节点树(译者注:这个节点就是指react element),并保存在内存当中。而后,这个节点树会被flush到相应的渲染平台上-举个例子,对于浏览器应用来讲,“节点树会被flush到相应的渲染平台上”具体点讲就是进行一系列的DOM操做。一旦app被更新过(经过调用setState),一个新的节点树就会被生成了。新的节点树会跟存在于内存当中的那颗旧的节点树进行比对,从而计算出更新整个app界面所须要的具体操做。

虽然,React Fiber是对reconciler的重写,可是依据React doc中对高层算法的描述,重写先后,reconciler仍是有大量的相同处。比较关键的两点是:

  • 假设不一样“组件类型”的组件会生成大致不一样的的节点树。对于这种状况(不一样“组件类型”的组件的更新),react不会对它们使用diff算法,而是简单粗暴地将老的节点树彻底替换为新的节点树。
  • 使用key这个prop来diff列表。key应该是“稳定(译者注:也就是说不能用相似于Math.random()来生成key的prop值),可预测的和惟一的”。

reconciliation与rendering

DOM只是React可以适配的的渲染平台之一。其余主要的渲染平台还有IOS和安卓的视图层(经过React Native这个renderer来完成)。

react之因此可以适配到这么多的渲染平台,那是由于react是被设计成两个独立的阶段:reconciliation和rendering。reconciler经过比对节点树来计算出节点树中哪部分须要更改;renderer负责利用这些计算结果来作一些实际的界面更新动做。

这种分离的设计意味着React DOM和React Native能使用独立的renderer的同时公用相同的,由React core提供的reconciler。React Fiber重写了reconciler,但这事大体跟rendering无关。不过,不管怎么,众多renderer确定是须要做出一些调整来整合新的架构。

Scheduling

Scheduling

决定什么时候(译者注:这是个关键词)应该执行work的那部分程序

work

须要被执行的全部计算。work(这个概念)通常意义下是做为一次更新的结果而存在的。

React的Design Principles文档在这个话题上阐述得很是好,所以,我在这里会引用它:

在当前实现中,React会递归地遍历节点树,相应地调用这颗已更新的节点树上的render方法。然而,在未来,React会延迟某些更新来避免界面更新的掉帧。

这是一个React设计中常见的主题。Some popular libraries implement the "push" approach where computations are performed when the new data is available. React, however, sticks to the "pull" approach where computations can be delayed until necessary.

React不是一个通用的数据处理类库。它是一个用于构建用户界面的类库。可以知道app哪些计算跟当前更新是相关的,哪些是不相关的,这种能力在构建用户界面方面有独到的优点。

若是某些东西用户经过屏幕看不到的话,咱们能够将它所关联的逻辑延迟执行。若是数据的到达速度比帧率还要快的话,咱们能够对数据进行合并,采用批量更新的策略 。咱们能够把来自于用户界面的work(好比说,经过一个button的点击触发了某个动画)的优先级定得比一些跑在后台,不过重要的work(经过网络请求把一些内容渲染到屏幕上)的优先级要高。经过这么作,咱们能够防止用户所看到的界面出现掉帧的现象。

上面给出的文档的关键点以下:

  • 在UI开发中,没有必要将每个更新请求付诸实施。实际上,若是真的是这么作的话,那么界面就会出现掉帧的现象,这大大下降了用户体验。
  • 不一样类型的更新请求应该由不一样的优先级。好比,执行动画的优先级应该要比一个来自于data store的更新的优先级要高。
  • push-based的方案要求你(开发者)须要手动去调度work。而pull-based的方案则可以让框架(react)学得变聪明,而后帮你去作这些调度工做。

react当前(2016年)对scheduling的应用度尚未足够大。一次更新意味着(简单粗暴地)对整个节点树进行一个完整的重渲染(译者注:级联式的层层更新)。重写react的核心算法就是为了利用scheduling带来的优点。这应该就是React Fiber项目的初心。

如今,咱们已经准备好深刻到React Fiber的实现当中去了。下一节,咱们将会提到愈来愈多的技术性东西。在继续阅读以前,请确保你对上面章节所提到的内容已经消化理解得差很少了。

什么是React Fiber?

咱们将要讨论React Fiber的核心了。React Fiber这个抽象层级比你想象中的还要低。若是你发现你本身在尝试理解这个架构的过程当中苦苦挣扎的话,不要气馁哈。不要放弃,继续尝试,你终将会弄明白它的(当你终于理解了React Fiber,麻烦给点关于完善这一小节内容的建议)。

咱们在上面已经确认了React Fiber的首要目标是使得React可以整合scheduling。更具体地来讲,是咱们能作到一下几点:

  • 中断work和稍后恢复work
  • 对不一样类型的work赋予至关的优先级
  • 复用以前已经完成的work
  • 若是一个work已经不须要继续了,中断它。

为了可以作到以上几点,咱们须要将work拆分红一个个的单元。这个单元其实就是React Fiber。一个React Fiber表明着一个work单元

再继续阐述前,咱们不妨重温一下这个概念:React components as functions of data,能够用通俗的方式来表达:

v = f(d) // view = f(data)
复制代码

所以,咱们能够作这样的等价的心智模型:渲染一个 React app 就等同于调用一个函数,而后这个函数体里面又包含了对其实其它函数的调用......与此类推。这种心智模型对理解React Fiber十分之有用。

计算机是经过call stack 来追踪程序的执行过程的。一个函数一旦被执行,它就会成为stack frame,加入到stack中。这一帧stack frame表明着这个函数所要完成的一个work。

问题就是,当跟UI打交道的的时候,一次性会有不少的work被执行。这会致使动画掉帧从而看起来不太流畅。还有就是,一些work若是会立刻被后面的work所取代的话,那么立刻执行这个work就显得不必了。This is where the comparison between UI components and function breaks down, because components have more specific concerns than functions in general.

新加入的渲染平台(好比react native)实现了一些用于专门处理这个问题的API:requestIdleCallback和requestAnimationFrame。requestIdleCallback负责将一些低优先级的函数安排在空闲期间执行;requestAnimationFrame负责将一个高优先级的函数安排在下一个动画帧期间调用。如今的问题是,为了可以使用这种API,你必须找到一种方法将rendering work拆分红各类增量单元(incremental units)。若是咱们仅仅依靠call stack,彷佛是不行的。由于call stack会一直执行work单元,直到call stack清空为止。

若是可以经过自定义call stack的行为来优化UI渲染岂不是很棒吗?若是咱们能手动地打断call stack和操做call stack的每个帧岂不是很棒吗?

而这就是React Fiber的目的。React Fiber是对stack的从新实现,特别是为了React组件而做的实现。你能够把单独的一个Fiber理解为一个virtual stack frame。

对stack的从新实现的好处是,你能将stack frame保存在内存中,本身想何时,在哪里执行均可以。这对于咱们实现引入scheduling时所设下的目标很重要。

重写stack,除了能实现scheduling以外,手动处理stack frames还能让实现一些新特性(好比:concurrency 和error boundaries)成为了可能。咱们将会在将来的一些章节来讨论这些话题。

在下一节,咱们更深刻地看看一个Fiber的数据结构。

Fiber的数据结构

注意:随着愈来愈深刻到技术细节,那么这些细节所面临更改的可能性也会随着增长。若是你发现了任何错误或者过时的信息,麻烦发个PR给我。

具体而言,一个fiber其实就是一个javascript对象。这个对象包含了一些关于组件的信息:这个组件的输入,这个组件的输出。

一个fiber对应于一帧stack frame,同时也对应于一个组件的实例。

如下是fiber对象的一些比较重要的字段(这个列表也不太详尽)。

type 和 key

fiber对象的type和key字段跟react element的type和key字段的含义是一致的。(实际上,fiber对象是从react element建立而来的,在建立的时候,这两个字段会被直接地copy过来)。

fiber对象的type字段描述的就是它所对应的组件。对于composite components来讲,这个type字段值就是一个function或者class component(译者注:本质上也是一个function)。对于host components(好比,div,span等)而言,type字段的值就是字符串。

理论上说,type字段的值就是被stack frame追踪它的执行的那个函数(v = f(d ))。

跟type字段同样,key字段被用于reconciliation期间决定是否要复用该fiber。

child 和 sibling

这两个字段都是指向其它的fiber,共同组成了具备递归结构的fiber树。

child fiber就是组件的render方法的返回值。举个例子:

function Parent(){
    return <Child />
}
复制代码

Parent的child fiber就是Child。

sibling字段存在于那些从render返回的多个children的身上(fiber的新特性):

function Parent() {
  return [<Child1 />, <Child2 />]
}

复制代码

上面全部的child fiber组成了一个单(向)链表。第一个child就是这个单链表的头节点。在这个示例中,Child1是Parent的child fiber,Child2是Child1的 sibling child。

不妨回头看看咱们以前用function作过的类比,你能够把child fiber理解为一个tail-called function

return

return fiber是指程序在处理完当前这个fiber所返回的那个fiber。概念上,它是等同于返回的stack frame的地址(It is conceptually the same as the return address of a stack frame)。咱们也能够把它看成parent fiber。

若是一个fiber有多个child fiber,这些child fiber的return fiber就是它们的parent fiber(此处的return有点是“上游”的意思)。因此,在咱们上面所提到的示例中, Child1和Child2的return fiber就是Parent。

pendingProps 和 memoizedProps

概念上说,props就是函数的参数。在函数刚开始执行的时候fiber的pendingProps就会被设置上,在函数执行完毕,fiber的memoizedProps也会被设置上。

当函数再次被执行,一个新的pendingProps会被计算出来,若是它与fiber的memoizedProps字段值相等的话,那么这就至关于告诉咱们fiber以前的output能够复用,从而阻止了没必要要的work。

pendingWorkPriority

一个用于指示优先级的数值。谁的优先级呢?fiber所表明的work的优先级。ReactPriorityLevel 模块列举出了全部work的优先级,而且也罗列了这些work所表明着什么。

除了“noWork”这个特例外(它的优先级数值是0),数值越大表明着优先级越低。举个例子,你能够用如下的函数去检查当前的fiber的优先级是否比给定的优先级高:

function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &&
         fiber.pendingWorkPriority <= priority
}
复制代码

上面这个函数只是用于演示而已,它并非来源于真实的react fiber源码。

scheduler就是经过消费fiber的priority字段来决定下一个须要执行的work单元是谁。这其中的算法将会在future section小节去讨论。

alternate

flush

当咱们说flush一个fiber时,意思就是将这个fiber的output渲染到屏幕上。

work-in-progress

当一个fiber还没被完成时(has not yet completed),咱们就说这个work是work-in-progress。概念上就是指某个stack frame还没被返回的时候。

在任什么时候候,一个组件实例最多对应着两个fiber:当前的,已经flushed的fiber和处在work-in-progress的fiber。

current fiber与work-in-progress的fiber会相互转化的(互生性)。current fiber最终会转化为work-in-progress的fiber,work-in-progress的fiber最终会转化为下一次work开始时的current fiber。

一个fiber的互生fiber是由一个叫cloneFiber的函数惰性地建立的。相比于老是建立一个新的对象,cloneFiber将会尝试复用它的互生fiber,若是它存在的话。这么作,可以减小内存分配。

alternate字段已然是react fiber的实现细节了。由于它在源码中的出现频率过高了,全部值得咱们在这里讨论一下它。

output

host component

React app的叶子节点。它们表明的是具体的渲染平台(例如,对于浏览器app来讲,DOM节点如“div”, “span”等就是 host component)。在JSX中,它们都是以小写的标签名出现的。

概念上说,一个函数的返回值就是一个fiber的output(这句话有歧义)。

每个fiber最终都会有本身的output,可是这个output的建立只能在叶子节点由host component来建立。output建立后,会沿着节点树往上传递。

output最终会传递给renderer,而后应用会把最新状态渲染在屏幕上。定义output是如何建立的,又是如何被更新到屏幕的,这些事就是renderer的职责之所在了。

Future sections

这是到目前为止的全部内容了。可是,这是一篇未完待续的文档。将来的一些章节里面,我会阐述一个更新过程当中自始至终所采用的算法。包含的主题以下:

  • scheduler是如何查找出下个要执行的work unit是谁?
  • fiber tree中,优先级是如何被追踪和传播的?
  • scheduler是如何知道何时暂停和恢复work的呢?
  • work是如何被flush掉,而且并标记为“已完成的”?
  • side-effect(好比生命周期方法)是如何运行的呢?
  • coroutine是什么?它是如何被用来实现某些特性(好比context和layout)的呢?

Related Videos

相关文章
相关标签/搜索