React源码系列(一)------ beginWork

991 阅读6分钟

在开始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更新到这颗树上,然后将正在作为视图呈现给用户的那颗树替换下来。具体看以下图示:

whiteboard_exported_image.png

每一次更新fiber树时都会拿替身和新VDOM去更新,我们称这种有替身轮流上岗的方式为双缓冲

(2)更新队列

更新队列这个小知识不影响下文对beginWork的理解,不打算调试代码的可跳过直接开始beginWork的流程,主要是给想要亲自调试代码的读者讲解的。

在react中,凡是遇到更新队列,基本都是一个单向循环链表的形式,基本都长这个样子:

image.png 由上图得知,更新队列有个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树的构建。

beginWork主流程.png

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的子链表,从更新队列获取子虚拟DOM3.2)tag === 5,构建原生节点的子链表,从pengdingProps中获取子虚拟DOM3.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.child5.3)无child,将null赋值给workInProgress.child。
          执行当前fiber的completeWork(这个是completeWork阶段的内容,此处不展开)。
          判断当前fiber是否有兄弟fiber,若有兄弟,则将兄弟fiber赋值给workInProgress,否则将父fiber赋值给workInProgress。
          返回第二步

(6)workInProgress = workInProgress.child;回到第二步
图片版流程图

beginWork.png

总结

beginWork构建fiber树的过程,主体逻辑是深度优先。先完成大儿子的fiber构建,然后是大儿子的兄弟,然后返回上一层,不断重复这个步骤,最后形成一棵树。

执行顺序:

  1. 如果有长子,先完成长子。
  2. 如果没有长子,标志节点遍历完成。
  3. 如果有弟弟,就继续完成弟弟。
  4. 如果没有弟弟,返回父节点,完成父节点,如果父节点有弟弟就继续完成父节点的弟弟。
  5. 如果没有父节点,结束。

结尾

这里是只有beginWork的代码的仓库分支,大家可亲自用这代码进行调试,过一遍整个beginWork流程。

ps:这部分的代码,函数名大多是基本与源码一致,若觉得笔者有错误的地方或想拓展哪部分,可通过函数名直接在源码中搜索。

这里是整个React源码,供大家拓展。

下一篇:首次挂载时completeWork