2461 字
12 分钟
React源码学习-fiber原理-part3

Fiber架构#

官方回答什么是fiber

Fiber并不是计算机术语中的新名词,他的中文翻译叫做纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。

在很多文章中将纤程理解为协程的一种实现。在JS中,协程的实现便是Generator

所以,我们可以将纤程(Fiber)、协程(Generator)理解为代数效应思想在JS中的体现。

React Fiber可以理解为

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态

其中每个任务更新单元React Element对应的Fiber节点

Fiber架构心智模型#

React核心团队成员Sebastian Markbåge (opens new window)React Hooks的发明者)曾说:我们在React中做的就是践行代数效应(Algebraic Effects)。

代数效应#

这篇文章可以好好看看

代数效应能够将副作用从函数逻辑中分离,使函数关注点保持纯粹。

就比如我们平时用await来等待一个值的返回

async function getData(){
const res = await loadData();
return res;
}

代数效应相当于,我关注的是 await loadData()能给我什么东西,而不是关注他里头是异步还是同步,怎么处理这个数据的;

代数效应 in React#

在react中有个结合Suspense的例子

Suspense Demo

而我们日常用hooks,例如useState什么的,我们不需要关注函数组件的这个state状态是怎么处理的,我们只要知道这个东西能干嘛就行。

为什么不用Generator#

新老架构那片说过,React16之后引入了一个 scheduler(调度器),并且重构了Reconciler(协调器),就是为了将react的老一套同步更新 的架构变成 **异步可中断 **的

异步可中断更新可以理解为:更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。

缺点:

  • generator它具有传染性和async await一样,就像你用async的函数那东西,你需要写await一样
  • 中间状态上下文相关,可以看看这个 解释

Fiber实现原理#

Fiber含义#

首先我们要明白:

  1. react15的时候采用递归方式去执行,数据保存在递归调用栈中,故被称为stack reconciler
  2. react16 的reconciler 基于 fiber节点实现的,故称为 fiber reconciler
  3. 一个 React Element 对应一个Fiber节点
    • 作为静态结构理解 --- 保存了该组件类型(原生/类组件/函数组件/…),以及对应dom节点信息
    • 作为动态结构理解 --- 每个Fiber保存了本次更新中该组件改变的状态,要执行的工作(插入/删除/更新…)

Fiber数据结构#

Fiber数据结构

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 作为动态的工作单元的属性
this.effectTag = NoEffect;
this.subtreeTag = NoSubtreeEffect;
this.deletions = null;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber, current tree中的fiber节点和workinprogress tree的fiber节点连接
this.alternate = null;
}

Fiber节点之间关系#

Fiber节点之间是通过以下三个属性建立起连接的

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

比如下面的代码和对应的Fiber树如下:

function App(){
return (
<div>
<span>hello world</span>
<a>
weng
<span/>
</a>
</div>
)
}

image-20210821213628488

这里需要提一下,为什么父级指针叫做return而不是parent或者father呢?因为作为一个工作单元,return指节点执行完completeWork(后面会说)后会返回的下一个节点。子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点。

作为静态的数据结构#

作为一种静态的数据结构,保存了组件相关的信息:

// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;

作为动态的工作单元#

作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息,我们会在后续的更新流程中使用到具体属性时再详细介绍

// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;

如下两个字段保存调度优先级相关的信息,会在学习Scheduler时说

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

Fiber工作原理#

首先,上面说了,Fiber节点直接会构成一棵Fiber树,并且存有节点以及各种信息每个Fiber。

所以一个Fiber是根据一个dom或者说React Element的出来的,那么树的结构也和dom或者组件树相同。

主要 更新 工作原理,这里用到了一个叫做 双缓存的技术

what is 双缓存?#

当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。

如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

这种在内存中构建并直接替换的技术叫做双缓存

React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

Fiber树和双缓存#

React中最多会同时存在两棵Fiber树

  1. Current Fiber 树:当前显示在屏幕面前的树
  2. workInProgress Fiber 树:正在内存中构建的树

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树rootFiber间切换来完成current Fiber树指向的切换。

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

每次状态更新都会产生新的workInProgress Fiber树,通过currentworkInProgress的替换,完成DOM更新。

mount时候#

考虑如下例子:

function App() {
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
  1. 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是“所在组件树的根节点。

之所以要区分fiberRootNoderootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode

fiberRootNodecurrent会指向当前页面上已渲染内容对应Fiber树,即current Fiber树

rootFiber

由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。

  1. 接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)

在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。

workInProgressFiber

workInProgressFiberFinish

update时#

  1. 接下来我们点击p节点触发状态改变,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树

wipTreeUpdate

这个决定是否复用的过程就是Diff算法,后面会说

  1. workInProgress Fiber 树render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后workInProgress Fiber 树变为current Fiber 树

currentTreeUpdate

查看源码中的FiberRootNode#

上面曾说到,我们首次创建react的应用的时候会创建整个应用的一个根节点 叫做 FiberRootNode

然后每次调用render方法都会创建当前组件的一个根节点叫做 rootFiber

这里我们验证以下这个 FiberRootNode

image-20210821223750870

图中圈出的部分就是react应用首次运行时候,创建一个**应用根节点FiberRootNode**的过程

我们顺着调用栈找到这个创建的调用方法 creatFiberRoot --- createFiber(创建应用根的方法)

image-20210821224000472

找到源码,打个断点

image-20210822153431608

刷新页面之后

image-20210822153527527

发现首次渲染,创建fiberRoot节点,这个tag为3,那么代表什么意思呢?

我们可以在它右边的调用栈中找到上层函数createHostRootFiber

image-20210822153638347

其实发现这个tag,也就是这里的HostRoot值为3

React源码学习-fiber原理-part3
https://nollieleo.github.io/posts/react源码学习-fiber原理-part3/
作者
翁先森
发布于
2021-08-21
许可协议
CC BY-NC-SA 4.0