第一章理念篇——1.4 深入理解Fiber

409 阅读8分钟

本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️

React18 源码系列会随着学习 React 源码的实时进度而实时更新:约,两天一小改,五天一大改。

Fiber架构的心智模型

代数效应——副作用从函数调用中分离

代数效应是函数式编程中的概念,用于将副作用从函数调用中分离,从而使函数关注点保持纯粹。

代数效应在 React 中的典型案例就是Hooks。对于类似useState、useReducer、useRef这样的Hook,我们不需要关注组件的state在Hook中是如何保存的。我们只需要假设useState返回的是我们想要的state,并照常编写业务逻辑就行。

function App() {
  const [num, updateNum] = useState(0);

  return (
    <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
  )
}

user.name用于展示用户名称。而用户名称是异步请求的,但是如下示例完全是同步的写法。

function ProfileDetails() {
  const user = resource.user.read();
  return <h1>{user.name}</h1>;
}

Generator——单一优先级任务的中断与继续

从React15到React16,协调器(Reconciler)重构的一大目的是:将老的同步更新的架构变为异步可中断更新。也就是说更新任务在执行过程中被打断(浏览器时间分片用尽或有更高优任务插队)的时候,react可以恢复之前执行的中间状态然后继续执行更新。

值得一提的是,浏览器原生支持通过Generator就实现类似的需求,但是Generator的一些缺陷使React团队放弃了他,原因主要有二:

第一,类似async,generator也是传染性的,使用了Generator则上下文的其他函数也需要作出改变。这样心智负担比较重。

第二,Generator执行的中间状态是上下文关联的,当我们只考虑“单一优先级任务的中断与继续”情况下Generator可以很好的实现异步可中断更新,但是当我们考虑“高优先级任务插队”的情况,Generator无法恢复之前执行的中间状态,如果通过全局变量保存之前执行的中间状态,又会引入新的复杂度。基于这些原因,React没有采用Generator实现协调器。

Fiber纤程——程序执行的过程(状态更新过程)

Fiber中文翻译叫做纤程,与进程(Process)、线程(Thread)、协程(Coroutine)同为程序执行过程。我们可以将纤程(Fiber)、协程(Generator)理解为代数效应思想在JS中的体现。我们可以把React Fiber可以理解为React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。我们可以把Fiber节点理解成每个任务更新单元。

Fiber的实现原理

Fiber的起源

Fiber 是虚拟 Dom 在React 中的正式称呼。

在React15及以前,协调器Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。

Fiber的含义

Fiber包含三层含义:

  1. 架构:之前React15的Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。多个Fiber节点依靠节点的相关属性连接形成树。
  2. 静态的数据结构:每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。
  3. 动态的工作单元:每个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.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

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

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

从架构上看

每个Fiber节点有个对应的React element,多个Fiber节点依靠 return、child、sibling三个属性连接形成树。

// 指向父级Fiber节点,也指子节点执行完completeWork后会返回的下一个节点。
this.return = null;
// 指向子Fiber节点,完成工作后会返回其父级节点
this.child = null;
// 指向右边第一个兄弟Fiber节点,完成工作后会返回其父级节点
this.sibling = null;
function App() {
  return (
    <div>
      i am
      <span>KaSong</span>
    </div>
  )
}

image.png

从静态的数据结构上看

保存了组件相关的信息

// 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;

如下两个字段保存调度优先级相关的信息

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

Fiber的工作原理

Fiber节点可以保存对应的真实DOM。Fiber节点构成的Fiber树就对应真实DOM树,之后会 Reconciler 通过“双缓存”技术确保真实Dom的最小化更新。

与我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面的理念类似,为了避免我们清除上一帧画面到绘制当前帧画面之间有较长间隙导致白屏和闪烁的问题,React使用“双缓存”(在内存中构建并直接替换的技术)来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

双缓存Fiber树

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

current Fiber树中的Fiber节点被称为current fiber,workInProgress 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树。

以mount时、update时的构建/替换流程为例:

mount时

function App() {
  const [num, add] = useState(0);
  return <p onClick={() => add(num + 1)}>{num}</p>;
}

ReactDOM.render(<App />, document.getElementById("root"));
  • 首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。

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

  • fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。fiberRootNode.current = rootFiber;
  • 由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。
  • 接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。
  • 在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。
  • 已构建完的workInProgress Fiber树在commit阶段渲染到页面。
  • fiberRootNode的current指针指向workInProgress Fiber树,使其变为current Fiber 树。

update时

  • 点击p节点触发状态改变,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树。

截屏2024-10-12 18.57.04.png

  • 和mount时一样,workInProgress fiber的创建可以复用current Fiber树对应的节点数据,这个决定是否复用的过程就是 Diff 算法
  • workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。

参考链接

关于作者

作者:Wandra

内容:算法 | 趋势 |源码|Vue | React | CSS | Typescript | Webpack | Vite | GithubAction | GraphQL | Uniqpp。

专栏:欢迎关注呀🌹

本专栏致力于每周分享一个项目,如果本文对你有帮助的话,欢迎点赞或者关注☘️