React 之 简易实现 Fiber架构

Posted lxcy_intellect

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React 之 简易实现 Fiber架构相关的知识,希望对你有一定的参考价值。

文章目录


此篇文章是在学习一步一步实现 fiber架构的同时,从另外一个由总到分的角度来总结 fiber架构的实现思路。文章末尾有一些学习参考文章可以借鉴。

fiber架构是什么?它解决了什么问题?

当我们项目过于复杂,渲染树过于庞大的时候,那么我们的递归渲染会耗时很长,而且很难被中断,fiber 的主要原理就是让我们在 diff 的过程中可以被中断,去处理更高优先级的事件如:用户事件或者动画,这样让浏览器的渲染更加流畅。

fiber 的核心思想,实现 fiber 我们需要做到什么?如何做?

React 16.0 之前,我们在渲染的过程中,通过去遍历一整棵 虚拟 dom 树来更新变化,我们很难中断,并且无法标记中断来持续工作。那么 fiber 架构将递归 diff 拆分成一个一个小任务,并且随时可中断,利用浏览器的空闲时间来执行,当处理完更高优先级任务后回到中断点继续执行;
要实现这样的机制,fiber做了什么,这里我总结了几点:

  1. 将原有的 vdom 树结构变成一个新的链表结构的树,每个节点都标记了它的(child,sibling,return,分别代表节点的第一个字节点、兄弟节点、父节点),这样可以随时中断,下次从中断处继续执行;
  2. fiber的核心思想是将一个庞大的任务拆分成一个个小的任务块,利用浏览器的空闲时间来执行,那么如何拆分,如何协调?React虚拟dom节点为维度对任务进行拆分,即一个虚拟dom节点对应一个任务,采用深度优先遍历的规则进行协调;
  3. 从根节点开始调度和渲染的过程可以分为两个阶段: render(),和 commit(); render 阶段会根据v-dom找出所有节点的变更(增删更新),然后构建出一棵Fiber 树,这个阶段是可以中断的。commit阶段就是将构建的Fiber three渲染成真实的dom, 这个阶段是不可中断的。
  4. 这里我们通过window.requestIdleCallback 来实现浏览器空闲时执行低优先级任务。

所以说 React Fiber 其实就是通过遍历将 VDom 转换成了 Fiber three,其中每个 Fiber都具有 child、singling、return属性;
遍历遵循深度优先遍历,自上而下,自左向右;从根节点出发,找到他的第一个子元素,找到则返回,没有则找他的兄弟元素,如果无兄弟元素,则直接返回其父元素, 父 ——> 第一个子 ——> 兄弟 ——> 父亲;
在遍历生成Fiber three 的时候根据节点的变更收集 effect list, 通过tag(UPDATE、DELETE、PLACEMENT),直到没有下一个任务,commit 到DOM树上。
在此之前,我们的 vdom 是一颗树,它在 diff 的过程中是没法中断的,于是将其改造成一个链表结构,之前是只有 children 进行递归遍历,现在是包含了父——>子, 子——> 父, 子——> 兄弟这几层关系的链表。

Fiber reconcile

至此,我们可以跟着思路来实现 fiber, 说到 fiber, 它其实就是一个具有各种标识的对象,如:


dom: null, // 真实dom,这里function 组件的dom是null
type,
props,
child,
return,
sibling,
alternate: null, // 旧值,用于比对更新
effectTag: 'PLACEMENT'

正题来了,首先我们还是来实现createElement(type, config, ...children) 最终返回 虚拟dom 树,这里不是重点,所以不过多介绍,详细可以查看createElement原理,直接贴代码:

/**
 * jsx语法糖,接受三个参数,返回v-dom(js对象)
 * @param * type 元素类型:native html | Function | Class
 * @param * config 属性
 * @param  ...any children 子元素
 * @returns v-dom 对象
 */
function createElement(type, config, ...children) 
  delete config.__self
  delete config.__source
  const  key, ref, ...rest  = config
  const vdom = 
    $$typeof: Symbol('react.element'),
    type,
    props: 
      ...rest,
      children: children.map(c => typeof c === 'object' ? c : createTextNode(c))
    
  
  return vdom

/**
 * 创建文本节点对象
 * @param * nodeValue 文本值
 * @returns v-dom 对象
 */
function createTextNode(nodeValue) 
  return 
    type: 'TEXT',
    props: 
      nodeValue,
      children: []
    
  

我们的核心是 任务拆解和任务协调,任务协调(reconcile)我们利用浏览器 API requestIdleCallback 来实现,React 实现了自己的任务调度函数,它接受一个callback(idleDeadline => ) ,利用 idleDeadline 我们能判断浏览器是否处于空闲时间来调度我们的任务。

/**
 * 任务循环
 * @param * idleDeadline 参数参考:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline
 */
function workLoop(idleDeadline) 
  // 当前还有空闲时间,可以设置超时 && 有任务可执行
  while (idleDeadline.timeRemaining() > 1 && nextUnitOfWork) 
    // 执行当前任务单元,并返回下一个待执行任务单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  
  // render循环结束的条件:无可执行任务,当前 effect list 已经生成, 开始commit了
  if (!nextUnitOfWork && wipRoot) 
    commitRoot(wipRoot)
  
  // 否则(当前忙碌,但是还有任务待执行), 继续监听等待执行
  requestIdleCallback(workLoop);

// 立即执行
requestIdleCallback(workLoop);

通过 idleDeadline.timeRemaining() 判断当前是否出于空闲时间,并且有下一个任务nextUnitOfWork可以执行, 循环执行当前任务,并返回下一个待执行任务;直到所有任务都执行完毕,我们将生成的 effect list 渲染成真正的 dom 树。当然,在有更高优先级事件正在处理的时,而我们还有 nextUnitOfWork未完成,那么继续调用 requestIdleCallback

由此可见,真正可中断的是 协调 (reconcile)阶段,在渲染阶段(commit)是不可中断的;

那么接下来我们要搞清楚的是如何对任务进行拆分?看下performUnitOfWork(nextUnitOfWork) 做了啥?

/**
 * performUnitOfWork 实质是通过遍历当前 fiber 节点的 children 来构建一颗小的Fiber three,最后根据遍历规则,如果有child,将其当成下一个任务返回
 * 否则,向上回溯到有sibling的父节点(return),作为 nextUnitOfWork。此时这里应该有一张图 TODO:
 * 这里的逻辑需要考虑首次渲染和更新操作,对Fiber进行effect标记。
 * @param * fiber 
 * @returns 下一个任务
 */
function performUnitOfWork(fiber) 
  // 这里需要区分 Native HTMl 和 函数组件, 类组件的实现后续会迭代更新TODO:,这里先用函数classTransferToFun进行转换
  if (fiber.type instanceof Function) 
  // 构建fiber three
    updateFunctionFiberThree(fiber)
   else 
    updateNativeFiberThree(fiber)
  
  // 至此,我们拿到了一棵小的Fiber three
  if (fiber.child) 
    return fiber.child
  
  let parentFiber = fiber;
  while (parentFiber) 
    if (parentFiber.sibling) 
      return parentFiber.sibling
    
    parentFiber = parentFiber.return // 属性 return 其实就是父节点
  
  return null

performUnitOfWork 的任务是接受一个fiber 节点,遍历其 children,深度遍历构建一棵小的fiber 链表, 将树上的每个 fiber 进行标识(child、sibling、return),最后依据“如果有第一个字节点,则返回子节点,否则返回其兄弟节点”的规则,来返回下一个任务的指定。

/**
 * 函数组件调度 children
 * @param * fiber 
 */
function updateFunctionFiberThree(fiber) 
  // 状态重置
  // .... 后面补充
  // 函数组件的type对应的就是函数fn,直接调用返回vdom
  const children = [fiber.type(fiber.props)] 
  reconcileChildren(fiber, children)

/**
 * HTML 元素 调度 children
 * @param * fiber 
 */
function updateNativeFiberThree(fiber) 
  if (!fiber.dom) 
    fiber.dom = createDom(fiber);
  
  reconcileChildren(fiber, fiber.props.children)

我们补齐了不同 type 的函数调度,其核心就是拿到其 children 进行遍历,构建fiber 节点,重点看 reconcileChildren 函数:

let deletions = []; // 存储需要标记删除的 fiber
/**
 * 通过 fiber 的 children,构建Fiber three
 * @param * fiber 
 * @param * children 
 */
function reconcileChildren(fiber, children) 
  // 如果有旧的Fiber,找到其子节点
  let oldFiber = fiber.alternate && fiber.alternate.child;
  let index = 0
  // 记录上一个 fiber, 用来构建 兄弟关系
  let prevFiber = null
  // 如果有子元素 或者 旧 Fiber 子元素存在
  while (index < children.length || oldFiber !== null) 
    let newFiber = null
    const node = children[index]
    // 对比新旧节点类型
    const sameType = oldFiber && node && oldFiber.type === node.type
    if (sameType) 
      newFiber = 
        dom: oldFiber.dom,
        type: node.type,
        props: node.props,
        return: fiber,
        alternate: oldFiber,
        effectTag: 'UPDATE' // 标记状态
      
    
    // 类型不同,新节点直接替换
    if (!sameType && node) 
      newFiber = 
        dom: null,
        type: node.type,
        props: node.props,
        return: fiber,
        alternate: null,
        effectTag: 'PLACEMENT'
      
    
    // 类型不同,旧节点存在, 将旧节点标记为删除
    if (!sameType && oldFiber) 
      oldFiber.effectTag = 'DELETE'
      deletions.push(oldFiber)
    

    if (index === 0) 
      fiber.child = newFiber
     else 
      // 将上一个节点的sibling指向当前节点
      prevFiber.sibling = newFiber;
    
    prevFiber = newFiber;

    // 移动指针,将oldFiber指向他的兄弟节点
    if (oldFiber) 
      oldFiber = oldFiber.sibling;
    
    index++;
  

这里考虑了节点的替换、删除和更新,通过字段effectTag进行标识,在更新的同时通过字段alternate记录上一次的fiber 节点,声明全局变量 deletions 用于存储删除的 fiber 节点,这些个字段均是用于再 commit 阶段用于判断 dom 如何挂载;

OK,至此,我们的整个reconcile 流程还差一步,设定初始 unitWork, 我们定义了一些全局变量,wipRoot 用于装载 整个 fiber 链表树 ,currentWipRoot 用于记录上一次更新的 fiber 链表树,用于更新比对,nextUnitOfWork为下一个待执行任务。

定义render(vnode, container) 函数,将容器元素作为第一个执行单元,重置 deletions

// 定义一些全局变量
let wipRoot = null; // 当前执行 Fiber three 的根节点,首次执行时为 document.getElementById('root') 对应的 Fiber
let currentWipRoot = null; // 记录上一次生成的 Fiber three,也可以说是触法更新之前的 Fiber three, 方便此次更新比对
let nextUnitOfWork = null; // 下一个待执行的任务块

/**
 * render 函数
 * @param * vdom  
 * @param * container 容器
 */
function render(vnode, container) 
  wipRoot = 
    dom: container,
    props: 
      children: [vnode]
    ,
    alternate: currentWipRoot // 用于记录上一次的状态,渲染时做比对
  
  deletions = []
  nextUnitOfWork = wipRoot

这里会留有一些疑问,currentWipRoot 什么时候赋值的?
循环执行直到最后一个nextUnitOfWork 执行完毕,此时 wipRoot 已经构建完毕,接下来进入 commit 阶段,正式构建真实 dom 树。

Commit 阶段

commit 阶段主要做的就是 dom 元素的 新增、删除和更新。
这里需要注意的事,在dom操作过程中,由于函数组件dom为 null, 通过return向上查找,直到找到存在dom的父节点为止,通过fiber 对象中的dom 字段可以拿到当前 fiber 的真实 dom。最后我们存储本次构建的 wipRoot, 用于下次更新进行比对。


/**
 * 渲染DOM树
 * @param * wipRoot 此次构建的 Fiber Three
 */
function commitRoot(fiberThree) 
  // 主要做了 件事:
  // 1. 将需要删除的deletions中的元素删除
  // 2. 更新变更,插入替换的新元素
  // 3. 将此次构建的 Fiber three 缓存在 currentWipRoot 中,下次触发更新时可做对比, 重置 wipRoot
  deletions.forEach(commitWorker)
  commitWorker(fiberThree.child)
  currentWipRoot = fiberThree 
  wipRoot = null


/**
 * fiber 的变更操作
 * @param * fiber 
 */
function commitWorker(fiber) 
  if (!fiber) 
    return
  
  let parent = fiber.return
  while (!parent.dom) 
    parent = parent.return
  
  const parentDom = parent.dom;

if (fiber.effectTag === 'DELETE' && fiber.dom) 
    deleteDom(fiber, parentDom)
  
  if (fiber.effectTag === 'PLACEMENT' && fiber.dom) 
    parentDom.appendChild(fiber.dom)
  
  if (fiber.effectTag === 'UPDATE' && fiber.dom) 
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  

  commitWorker(fiber.child);
  commitWorker(fiber.sibling);

补气 dom 的新增、删除和更新函数,这里只是做了一些代表型的处理,想更详细的了解可以看官方源码


/**
 * 生成dom
 * @param * fiber 
 */
function createDom(fiber) 
  const dom = fiber.type === 'TEXT' ? document.createTextNode('') : document.createElement(fiber.type)
  updateDom(dom, , fiber.props);
  return dom;


/**
 * 更新dom,遍历旧节点属性,将新节点中没有的属性删除; 遍历新节点,将旧节点属性更新,并新增旧节点没有的属性。
 * @param * dom 元素对象
 * @param * prevProps 
 * @param * nextProps 
 */
function updateDom(dom, prevProps, nextProps) 
  // 过滤掉 children , 得到新节点中没有的属性
  Object.keys(prevProps)
    .filter(pName => pName !== 'children')
    .filter(pName => !(pName in nextProps))
    .forEach(pName => 
      // 这里考虑事件处理函数的解绑,暂时只对click函数做处理
      if (pName.slice(0, 2) === 'on') 
        dom.removeEventListener(pName.slice(2).toLocaleLowerCase(), prevProps[pName], false)
       else 
        dom[pName] = ''
      
    )
  Object.keys(nextProps)
    .filter(pName => pName !== 'children')
    .forEach(pName => 
      if (pName.slice(0, 2) === 'on') 
        dom.addEventListener(pName.slice(2).toLocaleLowerCase(), nextProps[pName], false)
       else 
        dom[pName] = nextProps[pName]
      
    )


/**
 * 删除子元素,这里需要考虑 Function 组件Fiber无dom
 * @param * fiber 
 * @param * parentDom 
 */
function deleteDom(fiber, parentDom) 
  if (fiber.dom) 
    parentDom.removeChild(fiber.dom)
   else 
    deleteDom(fiber.child, parentDom)
  

好了,到目前为止,我们来写一个简单的示例,跑起来试试

import React from './fiber'
const ReactDOM = React

function Counter(props) 
    return (
        <div >
            <p>简易fiber架构实现</p>
            <span>props.count</span>
        </div>
    )

ReactDOM.render(<Counter count=5 />, document.getElementById('root'))

完美,页面正常显示

太简单了吧,但是我们还缺少对状态的更新处理,接下来我们就来实现hook 函数 useState, 来管理状态更新;

实现useState管理状态更新

首先我们声明两个全局变量
wipFiber 变量与函数组件一对一,将当前执行组件的 fiber 赋值给此变量,同时扩展 hooks 字段来对函数组件中多个状态进行存储,用 hookIndex来标识顺序,保证每次访问到正确的 state。

let wipFiber = null; // 与函数组件一一对应,存储组件fiber 和 hooks
let hookIndex = null; // 记录当前执行hook的指针,如组件中有多个useState hook 

/**
 * 函数组件调度 children
 * @param * fiber 
 */
function updateFunctionFiberThree(fiber) 
  // 状态重置
  wipFiber = fiber;
  wipFiber.hooks = [];
  hookIndex = 0;
  // TODO: 这里我觉得可以直接 用fiber.props.children, 应该是旧的?
  const children = [fiber.type(fiber.props)] // 函数组件的type对应的就是函数fn,直接调用返回vdom
  reconcileChildren(fiber, children)

我们在 updateFunctionFiberThree中追加对wipFiber 和 hookIndex 状态的重置,以此来隔离组件间的状态。当前时刻只有一个组件正在执行。

现在我们来实现 useState, 我们看看它在函数中如何使用

const [count, setCount] = useState(props.count)

可以看出,useState 接受一个初始值,在第一次执行时赋初始值,下次直接返回当前state的值;返回一个数组,分别是状态值和改变状态的函数

/**
 * 状态管理
 * 函数组件中可以通过声明多个 useState 来管理多个状态,我们通过顺序来管理多个状态
 * @param * defaultValue 初始值
 * @returns 
 */
function useState(defaultValue) 
  // 是否存在旧 hook 
  const oldHook = wipFiber?.alternate?.hooks && wipFiber.alternate.hooks[hookIndex];
  const hook = 
    // 读取状态的值,如果有,直接返回,如果是第一次,则初始化为 defaultValue
    state: oldHook?.state || defaultValue,
    queue: [] // 更新栈
  
  const actions = oldHook?.queue || [];
  actions.forEach(action => 
    if (typeof action === 'function') 
      hook.state = action(hook.state)
     else 
      hook.state = actions
    
  )
  const setState = (action) => 
    // 订阅更新操作
    hook.queue.push(action);
    // 设置 nextUnitWork 触发重新渲染
    wipRoot = 
      dom: currentWipRoot.dom,
      props: currentWipRoot.props,
      alternate: currentWipRoot
    
    nextUnitOfWork = wipRoot
    deletions = []
  
  wipFiber.hooks.push(hook)
  console.log(wipFiber)
  hookIndex++

  return [hook.state, setState]

我们通过 fiber.alternate 可以拿到上次render 时的 olderFiber, 同时即可拿到 hooks, 在setState 函数中只对action进行了push 操作,并设置 nextUnitWork 触发页面更新;
页面重新render, 函数再次执行,我们通过遍历执行oldHook?.queue 来更新state 并返回。

ok,我们对示例追加状态变更

import React from './fiber'
const ReactDOM = React

function Counter(props) 
    const [count, setCount] = React.useState(props.count)
    const handleClick = () => setCount(c => c + 1)
    return (
        <div >
            <p>简易fiber架构实现</p>
            <span>count</span>
            <br />
            <button onClick=handleClick>➕1</button>
        </div>

    )



ReactDOM.render(<Counter count=5 />, document.getElementById('root'))

可以看到能正常更新;
完整代码后续会整理到github。

参考文章

React源码之Fiber架构

什么是Fiber?

Fiber又命为纤程,他是与进程、线程、协程同为操作系统中程序的执行过程,可以将纤程理解为操作系统中的协程,那么generator也可以理解为协程,为什么没有使用generator来作为协程呢?

  • 当使用generator与async与await一样也是有传染性的,需要不断向上面声明*或者类似async与await中的async。
  • 实现更新可以中断并继续,实现更新可以拥有不同的优先级,高优先级的更新可以打断低优先级的更新,使用generator可以达到第一个目的,但是无法实现高优先级的更新可以打断低优先级的更新。

两种定义

静态数据结构

对于Fiber也作为一个静态数据结构,对于着一个组件保存了该组件的类型和对应的Dom节点等信息,这个时候的Fiber节点也就是我们所说的虚拟Dom。
在一个页面中可以有多个RootFiber,但是需要有一个FiberRootNode来管理这些RootFiber。

动态工作单元

每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除、被插入页面中、被更新…)。
对于Fiber我们可以理解为存储在内存中的Dom,在React15的协调器(Reconciler)是通过递归调用执行栈来实现的,在React16中的协调器(Reconciler)是基于Fiber节点来实现的。
对于React15在render阶段的reconcile是不可打断的,如果在操作大量的dom时,会存在卡顿,因为浏览器将所有的时间都交给了js引擎线程去执行,此时GUI渲染线程被阻塞,导致页面出现卡顿,无法响应用户对应的事件。
所以在React16之后就有了Scheduler来进行时间片的调度,给每一个task一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排,所以异步可中断的更新需要一定的数据结构在内存中保存dom信息,所以产出了这样一种数据结构Fiber,也可以称为虚拟Dom。

Fiber数据结构

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
  ) 
    // Instance
    // tag是Fiber对应的组件类型,比如Funtion Component、Class Component、Hooks Component
    // 其中Hooks Component是指Dom节点对应的Fiber节点
    this.tag = tag;
    this.key = key;
    // elementType大部分情况与type相同,除非Function Component使用React.memo来包裹时,他的ElementType与type不同
    // type对于Funtion Component来说是函数本身,对于Class Component是Class,对于Hooks Component是Dom节点的TagName
    this.elementType = null;
    this.type = null;
    // stateNode对于Hooks Components来说指对应的真实Dom节点
    this.stateNode = null;
    
    // Fiber
    // return/child/sibling会链接储存一颗Fiber树
    this.return = null;
    this.child = null;
    this.sibling = null;
    // 对于多个同级的Fiber节点,代表插入Dom的位置索引
    this.index = 0;
    // 就是我们常用的Ref属性
    this.ref = null;
    
    // 下面的属性都是将Fiber作为动态的工作单元使用时的属性 
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.updateQueue = null;
    this.memoizedState = null;
    this.dependencies = null;
    
    this.mode = mode;
    
    // Effects
    this.flags = NoFlags;
    this.subtreeFlags = NoFlags;
    this.deletions = null;
    
    // 调度优先级
    this.lanes = NoLanes;
    this.childLanes = NoLanes;
    // 关系到Fiber架构的工作方式
    this.alternate = null;
    
    if (enableProfilerTimer) 
      // Note: The following is done to avoid a v8 performance cliff.
      //
      // Initializing the fields below to smis and later updating them with
      // double values will cause Fibers to end up having separate shapes.
      // This behavior/bug has something to do with Object.preventExtension().
      // Fortunately this only impacts DEV builds.
      // Unfortunately it makes React unusably slow for some applications.
      // To work around this, initialize the fields below with doubles.
      //
      // Learn more about this here:
      // https://github.com/facebook/react/issues/14365
      // https://bugs.chromium.org/p/v8/issues/detail?id=8538
      this.actualDuration = Number.NaN;
      this.actualStartTime = Number.NaN;
      this.selfBaseDuration = Number.NaN;
      this.treeBaseDuration = Number.NaN;
    
      // It's okay to replace the initial doubles with smis after initialization.
      // This won't trigger the performance cliff mentioned above,
      // and it simplifies other profiler code (including DevTools).
      this.actualDuration = 0;
      this.actualStartTime = -1;
      this.selfBaseDuration = 0;
      this.treeBaseDuration = 0;
    
    
    if (__DEV__) 
      // This isn't directly used but is handy for debugging internals:
    
      this._debugSource = null;
      this._debugOwner = null;
      this._debugNeedsRemount = false;
      this._debugHookTypes = null;
      if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') 
        Object.preventExtensions(this);
      
    


Fiber双缓存

什么是双缓存呢?我们可以由动画来举例
动画由多张连续的图片组成,每一张图可以成为动画的一帧,当我们播放动画时需要有一定的速度连续展示每一帧,在渲染新的一帧的图片时需要将前一帧的图片清除,如果从清除前一帧图片到下一帧之间所展示的图片过长就会出现人眼能够感知的白屏闪烁,为了解决这个问题,我们可以从内存中绘制当前帧的图片,绘制完毕后直接用当前帧去替换掉上一帧的图片,由于这两帧替换出来所消耗的时间不会出现从白屏到画面闪烁的情况(速度足够快),这种在内存中构建并直接替换的技术称之为双缓存。
举例:
对于这样的Dom结构会生成如下的Fiber

function App() 
  return (
    <div>
      zi
      <p>bai</p>
    </div>
  )

ReactDOM.render(<App />, document.getElementById("root"));

对于上述结构的Fiber图如下:

mount

对于前面对于Fiber定义的讲解,我们知道了Fiber作为虚拟Dom可以保存和描述真实的dom,在mount的时候会先将jsx对象构建Fiber对象,形成Fiber树,这颗Fiber树叫做current Fiber并对应到真实Dom上。而正在构建的Fiber树叫做workInProgress Fiber,两棵树的节点会通过alternate相连。
对于首次进入页面的渲染时会通过ReactDom.Render来创建FiberRootNode,对于fiberRoot是指整个应用的根节点,只存在一个。每次调用ReactDom.Render都会创建当前应用的根节点RootFiber,对于rootFiber是指ReactDom.render或者ReactDOM.unstable_createRoot所创建出来的节点,可以存在多个其中会有一个current指针来指向RootFiber节点,由于在首屏渲染之前页面是空白的所以RootFiber没有子节点,接下来无论是首屏渲染还是调用this.setState或者调用useState的Hooks方法来创建的更新都会从根节点来开始创建一颗Fiber树。
在Mount时候只会创建FiberRootNode和rootFiber两个节点。初始时如下:

然后根据jsx创建workInProgress Fiber,然后通过alternate链接

接着workInProgress Fiber会和current Fiber交换位置,此时workInProgress变为current Fiber并渲染成对应的真实Dom则为如下图:

update

对于更新来讲,我们可以理解为动画帧,双缓存的替换类比为动画的两帧,当动画的下一帧替换动画的上一帧的速度够快就不会出现卡顿现象。此时我们来类比为Fiber,当更新时会在内存中生成workInProgress,然后将workInProgress替换为current Fiber并将current指针指向workInProgress Fiber并渲染为对应的Dom,如果执行速度够快就不会出现卡顿的现象。具体过程如下:
在update时会根据current Fiber与状态变更后的jsx对象做对比形成新的workInProgress Fiber,其过程也就是diff算法,然后workInProgress Fiber切换成current Fiber应用到真实dom就达到了更新的目的,这一切都是在内存中发生的,减少了对dom的操作。

最后再把workInProgress Fiber切换为current Fiber

上述为update时,current Fiber与workInProgress Fiber的变化

以上是关于React 之 简易实现 Fiber架构的主要内容,如果未能解决你的问题,请参考以下文章

react 多节点 diff 简易实现

React源码之Fiber架构

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber

读懂React原理之调和与Fiber