在开始react源码前,我们先简单了解一下fiber。
Fiber
- fiber是一种数据结构,每一个虚拟dom元素对应一个fiber,每个fiber通过相互间的关系进行连接。
看以下例子:
// 假设一个虚拟DOM长这样
<div> // 这个div对应的fiber我们暂且称之为div-fiber
<span>1</span> // 这个span对应的fiber我们暂且称之为span1-fiber
<span>2</span> // 这个span对应的fiber我们暂且称之为span2-fiber
<div>
以上这种情况,我们可以知道div有两个儿子分别为span1和span2,span1和span2为兄弟关系,那他们的fiber之间的连接就是这样的:
div-fiber.child -> span1-fiber // 没看错,这里父fiber只会指向长子
span1-fiber.sibling -> span2-fiber // sibling会指向比自己下一个的弟弟,无论有多少弟弟都只指向下一个
span1-fiber.return -> div-fiber // return会指向自己的父fiber
span2-fiber.return -> div-fiber
从上面这个例子可以看出,每一个fiber分别有child,sibling,return来指向另外一个fiber节点,正是因为这样的特性,所有的fiber组合起来,就是一颗树,这颗树的起点是最顶级的父fiber也就是div#root对应的fiber。
-
fiber是一个执行单位,也是最小执行单位。
-
react在构建整个fiber树的过程是可以被打断的,但它打断的节点处只能是一个fiber完成时,也就是说正在构建的fiber是无法被打断的,这也就是最小执行单位的含义。
React之所以要定义fiber这个概念,主要是因为react在执行构建渲染的过程不是连续的,react的工作是穿插在每一帧中的,它会从每一帧(一帧约16.6ms)中申请5ms的时间进行工作。因此将整个构建过程切片成一个一个小fiber,能做到这样的效果,这一个5ms完成了10个fiber的构建,下一个5ms可以开始下10个fiber得到构建,因为fiber之间的关系明确,可快速且正确得找到上一次中断的节点,这就使得整个构建过程成为了可中断,可暂停,可恢复的过程。
简单了解了fiber后我们开始今天的正题,beginWork。
beginWork
beginWork是react的第一阶段。
一、做了什么
beginWork的工作非常容易说清楚,那就是根据jsx给的VDOM生成一颗fiber树。
二、怎么做
补充三个小知识:
(1)双缓冲
react中始终存在着两颗fiber树,这两颗fiber树互为替身,每次更新的时候都是在闲置的那颗fiber树上进行工作,将所有更新后的VDOM更新到这颗树上,然后将正在作为视图呈现给用户的那颗树替换下来。具体看以下图示:
每一次更新fiber树时都会拿替身和新VDOM去更新,我们称这种有替身轮流上岗的方式为双缓冲。
(2)更新队列
更新队列这个小知识不影响下文对beginWork的理解,不打算调试代码的可跳过直接开始beginWork的流程,主要是给想要亲自调试代码的读者讲解的。
在react中,凡是遇到更新队列,基本都是一个单向循环链表的形式,基本都长这个样子:
由上图得知,更新队列有个pending属性,指向最后的一个更新,最后的更新有个next指向第一个更新,这样形成了一个单向循环链表。
(3)最基本的一个fiber所有用的属性
一个fiber所拥有的属性非常多,我们只讲比较重要的几个,其余的各位读者可看源码。
fiber = {
key: null, // 唯一标示
tag: 3, // tag类型,标示fiber对应的节点是什么类型,0:函数组件 1:类组件 3:根 5:原生节点 6 文本节点。还有很多其他tag,更多的请看源码
alternate: null, // 替身fiber
flags: 0, // 副作用标示
index: 0, // 自己是父fiber的第几个儿子
subtreeFlags: 0, // 子节点的副作用
updateQueue: null, // 更新队列
return: null, // 指向父fiber
child: null, // 指向长子fiber
sibling: null, // 指向下一个弟弟fiber
pendingProps: null, // 等待生效的属性
memoizedProps: null, // 已经生效的属性
memoizedState: null, // fiber的状态
};
以下正式开始beginWork流程的解析。
beginWork主流程
主流程其实很简单,就是创建出根fiber,然后将根fiber的子节点的VDOM添加到根fiber的更新队列中,然后再根据这个更新队列进行fiber树的构建。
fiber子链表的构建流程
在子fiber的构建过程中,有一个全局变量workInProgress,它就是当前fiber的替身,如果当前fiber的替身是null,那就创建一个新fiber替换掉这个null。它的具体作用,我们看一下整个fiber子链表的构建过程就了解了。
构建fiber子链表的流程以图片版流程图和文字版流程图展示。
文字版流程。
(1)将根fiber的alternate(替身)赋值给workInProgress,若这个替身为null,那就创建一个新的fiber赋值给workInProgress
(2)判断有无 workInProgress,若无就退出
(3)判断workInProgress类型(即是看tag为何值,这里只讲3个主要的类型,更多类型的处理可自行调试源码)
(3.1)tag === 3,构建根fiber的子链表,从更新队列获取子虚拟DOM
(3.2)tag === 5,构建原生节点的子链表,从pengdingProps中获取子虚拟DOM
(3.3)tag === 6,返回一个null并将null赋值给workInProgress。回到第2步。
(4)判断当前fiber有没替身,如果有替身,则表示本次为更新,标记需要跟踪副作用,无则标记不需跟踪副作用。
(5)根据获取到的子虚拟DOM,判断有几个child
(5.1)只有一个child。
创建这个child的fiber节点,将这个child的fiber赋值给workInProgress.child。
若需要跟踪副作用的给child的fiber的flags标记上插入操作。
(5.2)有多个child
遍历创建这些child的fiber,并将他们构成一个单向链表。
若需要跟踪副作用则给所有的child的flags标记上插入操作。
将长子fiber赋值给workInProgress.child
(5.3)无child,将null赋值给workInProgress.child。
执行当前fiber的completeWork(这个是completeWork阶段的内容,此处不展开)。
判断当前fiber是否有兄弟fiber,若有兄弟,则将兄弟fiber赋值给workInProgress,否则将父fiber赋值给workInProgress。
返回第二步
(6)workInProgress = workInProgress.child;回到第二步
图片版流程图
总结
beginWork构建fiber树的过程,主体逻辑是深度优先。先完成大儿子的fiber构建,然后是大儿子的兄弟,然后返回上一层,不断重复这个步骤,最后形成一棵树。
执行顺序:
- 如果有长子,先完成长子。
- 如果没有长子,标志节点遍历完成。
- 如果有弟弟,就继续完成弟弟。
- 如果没有弟弟,返回父节点,完成父节点,如果父节点有弟弟就继续完成父节点的弟弟。
- 如果没有父节点,结束。
结尾
这里是只有beginWork的代码的仓库分支,大家可亲自用这代码进行调试,过一遍整个beginWork流程。
ps:这部分的代码,函数名大多是基本与源码一致,若觉得笔者有错误的地方或想拓展哪部分,可通过函数名直接在源码中搜索。