[译]React Fiber 结构

329 阅读13分钟

原文地址:github.com/acdlite/rea…

简介

React Fiber是对React核心算法的重构,目前还正在进行中。它是React团队2年多以来不断研究积累的成果。

React Fiber的目标是使React在动画、布局、手势等领域的实现更好。它的中心特性是增量渲染:可以将渲染工作分为几个阶段,并散布在多个帧中执行。

另外一些重要特性是,在新的更新触发时,可以暂停、退出或者重新执行当前工作;可以给不同类型的更新工作设置不同的优先级;一种新的并发模式。

关于此文档

Fiber引入了一些新的概念,这些概念通过单纯看代码很难理解透彻。这篇文档是我的笔记集合,记录了我在React项目中,理解Fiber实现的一些心得。随着笔记的增多,我觉得这些笔记也许对其他人也有帮助。

我想尽可能的使用简单的语言描述,除了明确定义的关键条目,尽量避免使用其他术语。如果有必要,我也会提供链接,以供大家进行扩展阅读。

请注意,我并不在React的开发团队中,而且没有获得任何授权。这篇文档不是官方文档。为了保证准确,我请React团队中的成员检查了文档中的内容。

这是一项进行中的工作。*Fiber是一项进行中的工作,在正式发版前,将有可能会进行任何有意义的重构。*同样的,这篇文档也是进行中的工作。非常欢迎大家帮助我改进和提出任何意见。

我的目标是,在你阅读完此文档之后,可以理解Fiber的实现原理,而且最终甚至可以成为React的贡献者。

要求

我强烈建议你在继续之前,需要对以下知识点很舒旭:

  • React组件,元素,和实例 - “组件”是一个重要概念。对于这些条目的充分理解很关键。
  • Reconciliation(调和)- React的reconciliation算法的高度描述
  • React的基础理论概念 - 无关于实现的,对React概念模型的描述。其中有些在初次阅读时可能不能完全理解。不要担心,随着学习的深入,理解也会更深刻。
  • React的设计原则 - 需要多花一些注意力在这个部分上。它花了很大力气去解释为什么需要使用React Fiber。

回顾

请先对照自己的知识检查上面的“要求”部分。

在我们结束新事物之前,先回顾一些概念。

什么是reconciliation?

reconciliation

一种diff算法,用于对比2棵树,以决定哪些部分需要改变。

update

数据更新驱动React app的UI渲染。一般是setState的结果。最终引起重新渲染。

React API的核心是思考更新(updates)是否会引起整个app的重新渲染。允许开发者合理的生命,而不用去担心在APP中如何高效的从某特定状态转换到其他状态(A到B,B到C,C到A等等等等)。

事实上,每次修改就渲染整个app的方式只会在大多数小应用中存在;在一个真实的APP中,是禁止这种非常浪费性能的方式。React有最佳实践应用于创建整个APP的重新渲染并保持很好的性能表现。这种最佳实践被称之为reconciliation.

reconciliation算法是基于广为人知的“虚拟DOM(Virtual DOM)”。意思可以概括为:当你渲染一个React应用时,会创建一个节点数用于描述当前APP,并保存在内存中。例如,在浏览器应用中,这棵树会在浏览器中执行,将会修改一些DOM的属性。当一个应用需要更新(通常是通过setState触发),会产生一颗新的树。对比前后2棵树,以确定需要在渲染树上执行什么更新操作。

尽管Fiber是对reconciler的彻底重写,但和React文档中描述的高级算法基本上相同。关键点是:

  • 不同的组件类型被假设产生完全不同的树。React不会尝试对其进行diff,会直接用新的树替换掉老的树。
  • Diff的执行需要使用key。key必须保证稳定,可预测,唯一性。

对比Reconciliation和render

DOM仅仅只是React可以渲染的渲染环境中的一种,其他还有原生IOS和Android视图,可以通过React Native实现。(这就是为什么“虚拟DOM”这一用词有一点不当的原因)

React可以支持这么目标环境的原因是因为React被设计为reconciliation和渲染是分开的2个阶段。reconciler用于计算树中的哪些部分发生改变;渲染使用这些信息来重新渲染app。

这种分离的特性意味着React DOM和React Native可以使用个自己的渲染实现,同时共享由React core提供的reconciler。(有点机制与策略相分离的感觉)

Fiber重构reconciler。大部分的关注点并不是在渲染上,尽管渲染也需要修改以支持(并利用)这个新的结构。

Scheduling

scheduling

决定work何时执行的进程。 the process of determining when work should be performed.

work

任何需要被执行的计算。通常是updates的结果。(例如:setState)。 any computations that must be performed. Work is usually the result of an update(e.g. setState).

React 的设计理念对scheduling说的很清楚,我这里只是引用

在目前的实现里,React在一个tick里递归便利整个更新树,并执行render函数。但是在未来,可能会推迟某些更新,以避免掉帧的情况。 在React的设计中,这是一个常见的主题。某些库采用’push’方法,当一个新数据可用时,计算马上执行。但是,React支持‘pull’方法,计算可以推迟到必要的时候才执行。 React不是普通的数据进程库。它是一个为构建用户界面而生的库。我们认为它在一个APP里的唯一定位是,知道哪些计算是必须马上执行和哪些不是。 如果有些操作是幕后的,我们可以延迟任何与其相关的逻辑。如果数据获取速度比帧频快,我们可以联合并进行批量更新。我们可以提高用户交互(例如按钮点击引起的动画)相关的优先级,高于少量重要的幕后工作(例如渲染刚从网络获取的新内容),以避免掉帧。

关键点:

  • 从UI层面上说,立即执行每个更新是不必要的。实际上,如果这么做的话,是非常糟糕的,会引起掉帧和差的用户体验。
  • 不同类型的更新拥有不同的优先级 - 完成动画的更新速度要快于修改一个数据引起的更新。
  • 基于push的方式要求app(你,开发者)决定怎么安排work。基于pull的方法允许框架(React)变得聪明,并且帮助你做这些决定。

React目前没有使用Scheduling,整棵子树的更新是立即渲染的。Fiber结构的出现,驱动了React重构了核心算法以利用scheduling的优势。

现在我们开始学习Fiber的实现。下面的内容比我们之前讨论的内容更需要专业知识。在继续往下看之前,请确保你对前面的知识充分了解了。

什么是Fiber?

我们将要讨论React Fiber结构的核心部分。比起应用开发者的典型思考内容,Fiber是一种更低级(更基础?)的抽象。如果你在学习、理解的过程中,感到困惑,不要灰心。继续尝试,最终会明白的。(当你最终理解了,请提出你的建议,帮助本章的内容写的更好。)

现在开始吧!

我们先建议一个初始目标:Fiber是允许React利用scheduling的优势。特别提醒,我们需要能够做到:

  • 暂停和恢复work
  • 给不同类型的work分配优先级
  • 复用之前已经完成的work
  • 在不需要的时候退出work

为了完成上面的事情,我们首先需要一个种方式将work分离成独立的单元。从某种意义上来说,这就是Fiber。一个Fiber代表一个单元的work。

我们先回到React组件是以data为函数的参数这个概念上来,通常表示为:

v = f (d)

因此渲染一个React App类似于调用一个函数,这个函数内部去调用另一个函数,以此类推。这种类比在理解fiber上很有帮助。

计算机追踪一个程序执行的典型方式是使用调用栈(call stack)。当一个函数执行,一个*栈帧(stack frame)*入栈。这个stack frame代表这个函数被执行的工作(work)。

A stack frame is a memory management technique used in some programming languages for generating and eliminating temporary variables. In other words, it can be considered the collection of all information on the stack pertaining to a subprogram call.Stack frames are only existent during the runtime process.Stack frames help programming languages in supporting recursive functionality for subroutines. A stack frame also known as an activation frame or activation record. 出处

在处理 UI的问题时,如果同时执行太多工作,就会引起动画掉帧,看起来卡顿。更重要的是,其中有些工作是没有必要的,即使被最近的工作代替,也不会影响什么。这就是UI组件和函数崩溃之间的差异点,因为相比于普通函数,组件拥有更多的具体关注点。

更新的浏览器(和React Native)实现了一个API,用来帮助解决这个问题:requestIdleCallback调度一个低优先级的函数在空闲时间时调用;requestAnimationFrame调度一个高优先级的函数在下一帧动画开始之前调用。问题是,为了使用这些APIs,你必须把渲染工作分割为增量单元。如果你仅仅依赖调用栈,那么在调用栈空之前,它会一直保持工作。

如果我们能够自定义调用栈的行为,来优化渲染UI,这会不会超级棒?如果我们能够打断调用栈,并且手动操作栈帧,这会不会更棒?

这个就是React Fiber的目的。Fiber是对栈的重实现,特别是React组件。你可以把一个单独的fiber想象成一个虚拟栈帧(virtual stack frame)

重新实现调用栈的好处在于,你可以把栈帧保存在内存中,想什么时候执行就什么时候执行。这对我们完成调度的目标来说很关键。

除了scheduling,手动处理栈帧在未来能够解锁更多特性,例如concurrency模式和错误边界等。我们将会在未来的章节中了解这些内容。

在下一节中,我们将学习更多有关fiber结构的内容。

fiber的数据结构

注意:下列列出的特性,在未来可能会有修改。如果发现任何错误或者过时的信息,请提交一个pr。

具体来说,fiber就是一个JavaScript对象,包含了一个组件的信息,它的输入/输出。

一个fiber对应一个栈帧,同时也对应着一个组件实例。

下面是fiber结构上的一些重要字段。(没有列出完整的数据结构)

type and key

type和key这2个字段的目的是一样的,都是服务于React元素。(实际上,如果某个fiber是基于某个元素创建的,这2个字段是直接复制的。)

type描述的是与之对应的组件。对于复合组件而言,type是一个函数或者是组件类本身。对于原生组件(div, span, 等等),type是一个字符串。

从概念上来说,type是一个函数(像v = f (d) 一样),这个函数的执行需要被栈帧跟踪。

和type一起,key用于决定在reconciliation中,某个fiber是否能够重用。

child and sibling

这2个字段指向其他fiber,用于描述一个fiber的递归树结构。

child fiber对应着某个组件render方法返回的值。因此在下面的例子中

function Parent() {
	return <Child />
}

Parent的child fiber对应着Child。

sibling字段用于处理return返回多个children的情况(Fiber的新特性):

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

child fibers是一个单链表,它的头指针指向第一个child。因此在上面的例子中,Parent的child是Child1,Child1的sibling是Child2.

回想一下之前的函数类比,你可以把child fiber想成尾调用函数(tail-called function)

return

值是一个fiber,是程序执行完成之后应该返回的那个fiber。概念上类似于返回某个栈帧的地址。你也可以认为返回的是它的父 Fiber。

如果某个fiber有多个孩子fibers,每一个孩子的fiber返回的都是他们的父Fiber。因此在我们上面的例子中,Child1和Child2的return都是Parent。

pendingProps and memoizedProps

从概念上来说,props是函数的参数。fiber的pendingProps是在函数执行开始时设置的,memoizedProps是在函数结束时设置的。

当下一次的pendingProps与memoizedProps相等时,意味着这个fiber上一次的输出可以被重用,可以阻止没有必要的工作。

pendingWorkPriority

一个数字,表明fiber代表的工作的优先级。ReactPriorityLevel模块列出了不同的优先级,和他们代表了什么。

NoWork = 0,除了NoWork以外,数字更大,优先级越低。例如,你可以使用下面的函数,来检测某个fiber的优先级是否大于给定的水平:

function matchesPriority(fiber, priority) {
	return fiber.pendingWorkPriority !== 0 &&
			fiber.pendingWorkPrority <= priority
}

/上面的例子仅做展示,并不是React Fiber里的真实代码 /

scheduler根据这个优先级搜索下一个被执行的工作单元。这个算法将在未来的章节中讨论。

alternate

flush 将fiber的输出渲染到屏幕上 work-in-progress 未完成的fiber;从概念上来说,是一个还没有返回的栈帧

不管任何时候,组件实例最多有2个fibers与之对应: the current, flushed fiber,work-in-progress fiber。

const currentFiber = {
	alternate: workInProgressFiber
}

const workInProgressFiber = {
	alternate: currentFiber
}

Fiber的alternate通过cloneFiber方法惰性创建。相比于总是创建新的对象,cloneFiber将尝试复用已经存在的fiber的alternate,并最小化分配次数。

你应该从实现细节的方面来思考alternate。它经常出现在代码中,因此我们才有必要在这里讨论一下它。

output

host component React应用的叶子结点。他们由渲染环境决定具体内容(例如,浏览器中,他们是’div’,’span‘等等)。在JSX中,他们需要使用小写字母。

从概念上来说,fiber的output是函数的返回值。

每一个fiber最终都会有output,但是output只能由host component在叶子结点上创建。然后output被输送到树上。

output是最终传递给渲染者的内容,以便把修改刷新到渲染环境中。渲染着的职责就是定义output如何创建以及更新。

未来的章节

以上就是全部内容了,但是这篇文档还没有结束。未来的章节我们将会讨论更新过程中使用的算法。主要包含以下内容:

  • scheduler如何寻找下一个执行工作单元
  • 优先级如何被跟踪以及在fiber中如何传播
  • scheduler如何知道应该在何时暂停和重启工作
  • work如何刷新并标记完成
  • 副作用(例如生命周期方法)如何工作
  • 什么是协同程序以及如何利用协同程序实现context和layout等特性