Fiber

# Fiber架构的心智模型
# 代数效应
代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离 接下来我们用虚构的语法来解释下代数效应。
function getTotalPicNum(user1, user2) {
const picNum1 = getPicNum(user1);
const picNum2 = getPicNum(user2);
return picNum1 + picNum2;
}
2
3
4
5
6
函数getTotalPicNum,传入2个用户名称后,分别查找该用户在平台保存的图片数量,最后将图片数量相加后返回。
在getTotalPicNum中,我们不关注getPicNum的实现,只在乎“获取到两个数字后将他们相加的结果返回”这一过程。
接下来我们来实现getPicNum
用户在平台保存的图片数量是保存在服务器的,所以,我们需发起异步请求
为了尽量保持getTotalPicNum的调用方式不变,首先想到了async await
async function getTotalPicNum(user1, user2) {
const picNum1 = await getPicNum(user1);
const picNum2 = await getPicNum(user2);
return picNum1 + picNum2;
}
2
3
4
5
6
但是,async await是有传染性的——当一函数变为async后,调用他的函数也需async,破坏了getTotalPicNum的同步特性。
有什么办法能保持getTotalPicNum现有调用方式不变的情况下实现异步请求呢?
没有,不过可虚构一个。
虚构一个类似try...catch的语法 —— try...handle与两个操作符perform、resume
function getPicNum(name) {
const picNum = perform name;
return picNum;
}
try {
getTotalPicNum('kaSong', 'xiaoMing');
} handle (who) {
switch (who) {
case 'kaSong':
resume with 230;
case 'xiaoMing':
resume with 122;
default:
resume with 0;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
当执行getPicNum方法时,会执行perform name,之后name会作为handle的参数。
与try...catch最大的不同在于:当Error被catch捕获后,之前的调用栈就销毁了。而handle执行resume后会回到之前的perform的调用栈。
从例子中可以看出,perform resume不需要区分同步异步。
总结一下:代数效应能够将副作用(例子中为请求图片数量)从函数逻辑中分离,使函数关注点保持纯粹
# 代数效应在React中的应用
代数效应在React中最明显的例子就是Hooks。
对于类似useState、useReducer、useRef这样的Hook,不需关注FunctionComponent的state在Hook中是如何保存的,React会处理。
我们只需假设useState返回的是我们想要的state,并编写业务逻辑就行。
function App() {
const [num, updateNum] = useState(0);
return (
<button onClick={() => updateNum(num => num + 1)}>{num}</button>
)
}
2
3
4
5
6
7
这个例子不够明显,可看官方的Suspense Demo (opens new window) 在Demo中ProfileDetails用于展示用户名称。而用户名称是异步请求的。
但是Demo中完全是同步的写法。
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
2
3
4
# 代数效应与Generator
React从15到16,协调器(Reconciler)重构的一大目的是:将老的同步更新架构变为异步可中断更新。
异步可中断更新可理解为:更新在执行过程中可能会被打断,当可以继续执行时恢复之前执行的中间状态。
这就是代数效应中try...handle的作用。
其实,浏览器原生支持类似的实现Generator
但它的一些缺陷是React放弃了它:
- Generator也是有传染性的,使用它则上下文的其他函数也需作出改变
- Generator执行的中间状态是上下文关联的
举个例子
function* doWork(A, B, C) {
var x = doExpensiveWorkA(A);
yield;
var y = x + doExpensiveWorkB(B);
yield;
var z = y + doExpensiveWorkC(C);
return z;
}
2
3
4
5
6
7
8
当浏览器有空时都会依次执行其中一个doExpensiveWork,当时间用尽则中断,当再次恢复时则会从中断位置继续执行。
只考虑单一优先级任务的中断与继续时,Generator可很好的实现异步可中断更新。
但当有高优先级任务插队时,若此时已完成A与B计算出x与y。
此时B组件收到一高优更新,由于Generator执行的中间状态是上下文关联的,计算y时无法复用之前已得的x,需重新计算。
若通过全局变量保存之前执行的中间状态,又会引入新的复杂度。
更详细的解释可参考这个issue (opens new window)。
基于这些原因,React没有采用Generator实现协调器。
# 代数效应与Fiber
Fiber中文叫纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。 在很多文章中将纤程理解为协程的一种实现。在js中,协程的实现便是Generator。 所以可将纤程、协程理解为代数效应思想在JS中的体现。 React Fiber可理解为: React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,且恢复后可复用之前的中间状态。 其中每个任务更新单元为React Element对应的Fiber节点。
# Fiber架构的实现原理
在React15及以前,Reconciler采用递归的方式创建虚拟DOM,此过程不可中断。若组件树层级很深,递归会占用线程很多时间,造成卡顿。 Fiber的诞生解决了这个问题。
# Fiber的含义
Fiber有三层含义:
- 作为架构来说,之前的React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,被称为
stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler - 作为静态数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件、类组件、原生组件...)、对应DOM节点等信息。
- 作为动态的工作单元来说,每个Fiber节点保存了本次更新中组件改变的状态、要执行的工作(需被删除、被插入页面中、被更新...)
# Fiber的结构
从这里 (opens new window)可看到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.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 作为架构来说
每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
2
3
4
5
6
举个例子,如下的组件结构
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}
2
3
4
5
6
7
8
对应的Fiber树结构:
这里注意:为什么父级指针叫return而不是parent或father?
作为一个工作单元,return指节点执行完completeWork后会返回下一个节点。子Fiber节点及兄弟节点完成工作后会返回其父级节点。
# 作为静态数据结构
作为一种静态的数据结构,保存了组件相关的信息:
// 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;
2
3
4
5
6
7
8
9
10
# 作为动态的工作单元
作为动态的工作单元,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;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如下两个字段保存调度优先级相关的信息,会在讲解Scheduler时介绍。
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
2
3
# Fiber架构的工作原理
# 双缓存
当用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。
若当前帧画面计算量较大,导致清除上一帧画面到绘制当前帧画面间有较长间隙,会出现白屏。
为解决此问题,可在内存中绘制当前帧动画,绘制完后直接替换上一帧画面,省去了两帧替换间的计算时间,不会出现白屏。
这种在内存中构建并直接替换的技术叫双缓存 (opens new window)。
React使用双缓存来完成Fiber树的构建与替换——对应DOM树的创建与更新。
# 双缓存Fiber树
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树为current Fiber树,正在内存中构建的Fiber树为workInProgress Fiber树。
current Fiber树中的Fiber节点叫currentFiber,workInProgress Fiber树中的Fiber节点叫workInProgressFiber。它们通过alternate属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
2
React应用的根节点通过是current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。
每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。
以具体例子讲解mount时、update时的构建/替换流程。
# mount时
function App() {
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
2
3
4
5
6
7
8
- 首次执行
ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot,是整个应用的根节点)和rootFiber(<App/>所在组件树的根节点)。 为什么要区分两者呢?因为在应用中可多次调用ReactDOM.render渲染不同的组件树,它们会拥有不同的rootFiber。但整个应用的根节点只有一个。fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树
fiberRootNode.current = rootFiber;
由于是首屏渲染,页面没挂载任何DOM,所以rootFiber无子Fiber节点。
2. 进入render阶段,组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,为workInProgress Fiber树
下图中右侧为内存中构建的树,左侧为页面显示的树。
在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性。
在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。
3. 图中右侧已构建完的workInProgress Fiber树在commit阶段渲染到页面。
此时DOM更新为右侧对应的样子。fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树。

# update时
- 接下来点击
p节点触发状态改变,开启一次新的render阶段并构建一课新的workInProgress Fiber树
和mount时一样,workInProgress fiber的创建可复用current Fiber树对应的节点数据。这个决定是否复用的过程就是Diff算法。 workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。
