学习与理解 React Fiber
React 在 v16
引入了众多新特性,其中最核心的更新属于引入了新的核心架构 Fiber (Fiber reconciler,代替之前的 Stack reconciler),本文主要是对 fiber 的学习过程的记录。
一、为什么需要 Fiber ?
长话短说就是:性能。
In its current implementation React walks the tree recursively and calls render functions of the whole updated tree during a single tick.
React 15 及之前版本,协调算法(Stack Reconciler)会一次同步处理整个组件树。它会递归遍历每个组件(虚拟DOM树),去比较新旧两颗树,得到需要更新的部分。这个过程基于递归调用,一旦开始,很难去打断。也就是说,一旦工作量大,就会堵塞整个主线程(The main thread is the same as the UI thread.)。
而事实上,我们的更新工作可能并不需要一次性全部完成,比如 offscreen 的 UI 更新并不紧急,比如 动画 需要优先完成——我们可以根据优先级调整工作,把diff过程按时间分片!
If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.
所以 React 引入了 Fiber。
二、Fiber 是什么?
Fiber 的基本目标是可以利用 scheduling(scheduling 即决定工作什么时候执行),即可以:
- 暂停工作,并在之后可以返回再次开始;
- 可以为不同类型的工作设置优先级;
- 复用之前已经完成的工作;
- 中止已经不再需要的工作。
要达成以上目标,首先我们必须能把工作分成小单元(break work down into units)。从这一点来说,A fiber represents a unit of work。
进一步讲,React 的一个核心概念是 UI 是数据的投影 ,组件的本质可以看作输入数据,输出UI的描述信息(虚拟DOM树),即:
ui = f(data)
也就是说,渲染一个 React app,其实是在调用一个函数,函数本身会调用其它函数,形成调用栈。前面我们已经讲到,递归调用导致的调用栈我们本身无法控制,
只能一次执行完成。而 Fiber 就是为了解决这个痛点,可以去按需要打断调用栈,手动控制 stack frame——就这点来说,Fiber 可以理解为 reimplementation of the stack,即 virtual stack frame。
React Fiber is a virtual stack frame, with React Fiber being a reimplementation of a stack frame specialized for React components. Each fiber can be thought of as a virtual stack frame where information from the frame is preserved in memory on the heap, and because info is saved on the heap, you can control and play with the data structures and process the relevant information as needed.
三、Fiber 的简易实现
这一节本来是要直接去探索 React 怎么实现 Fiber 的。但 Rodrigo Pombo 有篇非常棒的自定义 Fiber 实现博文,这里先讲一讲这个实现,有助于我们理解 Fiber 到底是什么,是怎么实现手动控制 stack frame 的。
我阅读了 Rodrigo Pombo 的实现,并用 typescript 重写了一遍(有助于我自己理解),并加上了详细的注释(理解有谬误的大家可以帮忙提出):blog/codes/didact/src/reconciler.ts Lines 1 to 459 in a660a47
1、原作者的博客还是很易读易懂的,这里不再赘述。下面主要列出一些帮助理解的重点,在具体实现中,一个 fiber 可以理解为一个纯 JavaScript 对象,对应一个component:blog/codes/didact/src/interface.ts Lines 44 to 69 in a660a47
2、React 中 reconciliation 和 render 是两个独立的过程,其中 reconciliation 过程是纯粹的 virtual dom diff,不涉及任何 DOM 操作——这是我们为什么能够把 reconciliation 分割为多个工作单元 (unit of work) 的原因。而 didact 中是怎么分割/设置工作单元呢?
didact 中,reconciliation 可以理解为是创建 work-in-progress fiber tree 的过程。从 root fiber 开始,每处理一个 fiber 都是一个工作单元。每个 fiber 的处理过程基本是:
- 如果没有 stateNode,则创建(离线的DOM node或者是创建 class component的实例);
- 通过 props.children 或者 instance.render() 的返回值去创建 fiber 的 children fibers(effectTag 和 effects 存储了后面commit phase需要的 DOM 操作)。
3、通过 requestIdleCallback
API 来 schedule 工作;同时以 nextUnitOfWork
为下一步需要执行的工作对象。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Vue 源码解析一:observer
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
有点没搞明白的就是为啥不在beginWork的时候就把effect收集起来,而是再从子节点回溯回去的时候收集。难道遍历打effectTag的时候不能更新这个updateQueue么?
这个挺好的
非常好,期待继续