creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.62k stars 211 forks source link

学习与理解 React Fiber #44

Open creeperyang opened 6 years ago

creeperyang commented 6 years ago

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 重写了一遍(有助于我自己理解),并加上了详细的注释(理解有谬误的大家可以帮忙提出):

https://github.com/creeperyang/blog/blob/a660a478f14ce08a426a57e24cccce1acf652e30/codes/didact/src/reconciler.ts#L1-L459

原作者的博客还是很易读易懂的,这里不再赘述。下面主要列出一些帮助理解的重点:

  1. 在具体实现中,一个 fiber 可以理解为一个纯 JavaScript 对象,对应一个component:

https://github.com/creeperyang/blog/blob/a660a478f14ce08a426a57e24cccce1acf652e30/codes/didact/src/interface.ts#L44-L69

  1. 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 操作)。
  2. 通过 requestIdleCallback API 来 schedule 工作;同时以 nextUnitOfWork 为下一步需要执行的工作对象。

四、从 React 源码来看 Fiber

OwlAford commented 6 years ago

非常好,期待继续

CollinPeng commented 6 years ago

这个挺好的

EggTronic commented 3 years ago

有点没搞明白的就是为啥不在beginWork的时候就把effect收集起来,而是再从子节点回溯回去的时候收集。难道遍历打effectTag的时候不能更新这个updateQueue么?