React 源码阅读 - Fiber

658

上一篇文章我们给出了一个例子,但是只说了 React.createElement, 留下了 ReactDOM.renderReactDOM.render 内含的操作就非常多了,所以必须进行拆解。

我们把 ReactDOM.render 按照功能及调用顺序来划分可以分成三大块:schedule (调度)、reconciliation(协调)、render(渲染)。

简单说一下这三大块都干了什么事情:

  • scheduler(调度):调度任务的优先级,高任务优先进入下一阶段,也就是 Reconciliation(协调) 阶段
  • reconciliation(协调):找出变化了的组件
  • render(渲染):将变化的组件渲染到页面上

因为做的事情非常多,所以 ReactDOM.render 的调用栈特别深,直接看代码一个一个去梳理很容易蒙圈,所以我找了一个 React 生成的界面,使用 Chrome 的 Performance 进行录制,可以看到其调用栈如下图所示:

react1.png

上图的三个框基本上就代表了三大功能块儿。

我们不按照三大功能块儿的调用顺序来理解,因为这样去学习 react 源码可能会比较困难,所以我打算先搞清楚到底什么是 Fiber,这是一个贯穿始终的东西。

什么是 Fiber

Fiber 是一种特殊的数据结构(一个特殊的链表,链接着父兄子),也是一种虚拟 DOM 的实现,支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

一个 ReactElement 会对应一个 Fiber对象。

源码

Fiber 的源码在 packages/react-reconciler/src/ReactFiber.new.js路径下

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; // 待使用的 props
  this.memoizedProps = null; // 已使用的 props
  this.updateQueue = null; // 状态更新或回调链表
  this.memoizedState = null; // 状态
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags; // fiber 节点包含的副作用标识
  this.subtreeFlags = NoFlags; // 子树包含的副作用标识,避免深度遍历
  this.deletions = null; // 删除的节点,用于执行 unmount 钩子

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

  // 指向该 fiber 在另一次更新时对应的 fiber
  this.alternate = null;

  // 省略后面一些代码
}

解释几个比较重要的属性:

  • tag 当前节点类型的一个标记,比如:根节点的 tag 为 3;函数组件 tag 为 0。ReactWorkTags.js有完整定义
  • key 就是 key属性
  • stateNode Fiber 对应的真实 DOM 节点
  • elementType 是真实 DOM 元素的标签,比如:h3、div 等等
  • return、child、sibling 分别是父、子、兄 fiber节点
  • Effects 相关的属性,记录着是否需要更新或删除的一个标记,ReactFiberFlags.js 有完整的定义
  • lanes 是调度阶段使用的
  • alternate 是上一次渲染的时候的 fiber

双缓存

什么是双缓存

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

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

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

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

React 中的双缓存

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

在 React 中最多会同时存在两棵 Fiber树。当前屏幕显示的Fiber树current Fiber树,正在内存中构建的 Fiber树称为 workInProgress Fiber树,他们通过alternate属性连接。

根 Fiber

React 首次加载界面的时候会创建一个 FiberRootNoderootFiber

FiberRootNode 是整个容器的根节点,整个容器只有一个。

rootFiber 是组件树的根节点,它也是一个 FiberNode,可以有多个,不同的组件树其组件树的根节点也不一样。

FiberRootNodecurrent 属性会指向当前页面上已经渲染内容对应的 Fiber树,即 current Fiber树

首次渲染的时候,页面中还没有挂载任何 DOM,所以 FiberRootNodecurrent 属性为空,current Fiber树就为空。

接下来进入reconciliation 阶段的 render 阶段,根据组件返回的 JSX 在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树

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

下面是 FiberRootNode 的源码,路径为 packages/react-reconciler/src/ReactFiberRoot.new.js

function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  this.containerInfo = containerInfo;
  this.pendingChildren = null;
  this.current = null;
  this.pingCache = null;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.callbackNode = null;
  this.callbackPriority = NoLane;
  this.eventTimes = createLaneMap(NoLanes);
  this.expirationTimes = createLaneMap(NoTimestamp);

  this.pendingLanes = NoLanes;
  this.suspendedLanes = NoLanes;
  this.pingedLanes = NoLanes;
  this.expiredLanes = NoLanes;
  this.mutableReadLanes = NoLanes;
  this.finishedLanes = NoLanes;

  this.entangledLanes = NoLanes;
  this.entanglements = createLaneMap(NoLanes);

  // 省略部分代码
}

createHostRootFiber 函数会帮助初始化 current属性。

实例

import React from 'react';
import ReactDOM from 'react-dom';

function Com() {
  return (
    <div style={{ color: 'red' }}>
      <h3>Hello, Eagle!</h3>
    </div>
  )
}

ReactDOM.render(
  <Com />,
  document.getElementById('root')
);

下面是 div 对应的 FiberNode 的部分属性的一个展示:

alternatenull
childFiberNode {
    alternate: null
    child: null
    childLanes: 0
    dependencies: null
    elementType: "h3"
    firstEffect: null
    flags: 0
    index: 0
    key: null
    lanes: 1
    lastEffect: null
    memoizedProps: null
    memoizedState: null
    mode: 8
    nextEffect: null
    pendingProps: {children: "Hello, eagle!"}
    ref: null
    return: FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: null, …}
    selfBaseDuration: 0
    sibling: null
    stateNode: null
    tag: 5
    type: "h3"
    updateQueue: null
}
childLanes0
dependenciesnull
elementType"div"
firstEffectnull
flags2
index0
keynull
lanes0
lastEffectnull
memoizedPropsnull
memoizedStatenull
mode8
nextEffectnull
pendingProps: {
    children: {$$typeof: Symbol(react.element), type: "h3", key: null, ref: null, props: {…}, …}
	style: {color: "red"}
}
refnull
returnFiberNode {
    alternate: FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …}
    child: FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: null, …}
    childLanes: 0
    dependencies: null
    elementType: null
    firstEffect: null
    flags: 0
    index: 0
    key: null
    lanes: 0
    lastEffect: null
    mode: 8
    nextEffect: null
    pendingProps: null
    ref: null
    return: null
    sibling: null
	stateNode: FiberRootNode {tag: 0, containerInfo: div#root, pendingChildren: null, current: FiberNode, pingCache: null, …}
	tag: 3
	type: null
	updateQueue: {baseState: {…}, firstBaseUpdate: null, lastBaseUpdate: null, shared: {…}, effects: null}
}
siblingnull
stateNodenull
tag5
type"div"
updateQueuenull

通过上面这个对象可以看出一下几点:

  1. 当前 fiberreturn 属性是他的父节点,也是组件的根节点,也是 FiberNodeRootcurrent 属性;
  2. 当前 fiber 的父节点的 stateNode 属性对应着 FiberNodeRoot,可以看到他的 containerInfo 就是我们的真实 DOM 的 root 节点;
  3. 当前 fiberpendingProps 属性包含着当前节点的属性;
  4. 当前 fiberchild 属性是他的子节点,也就是我们的 h3 节点。