React源码系列(三):Fiber架构

前言

在React16版本之前,采用了递归的方式进行VDOM对比,也是由此导致更新一旦开始,就无法中断,如果VDOM Tree层级较深,VDOM对比就长时间占据主线层,无法执行其他任务,造成页面卡顿现象;

在React16之后,引用了Fiber架构,在新的架构中,Reconciler中的更新流程从递归变成了"可中断的循环过程",也就是采用循环模拟递归,VDOM的对比过程放在了浏览器空闲时间,不会长期占据主线程,从而解决VDOM对比造成的页面卡顿的问题;

Fiber Tree可以理解为是React自定义的一个用来描述dom之间链接关系的一个树,由每一个React Element 所对应的fiber Node 链接而组成。每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。

FiberNode是VDOM在React中的实现

1、react的三种节点类型

  1. React Element(React 元素) :即ReactElement方法或者JSX方法的返回值
  2. React Component(React 组件) :函数组件 类组件
  3. FiberNode:组成Fiber架构的节点类型

三者关系如下

javascript 复制代码
function App(){
  return <h1>hello world</h1>
}

const element = <App />

ReactDom.createRoot(rootNode).render(<App/>)

App 是React Component
element 是React Element
在React运行时内部,App对应的FiberNode                         

ReactDom.createRoot() 方式执行时,会创建FiberNodeRoot

root.render()方法运行时,会创建hostRootFiber, 也就是div#root对应的fiberNode

这张图所表述的关系表述了我们书写的JSX代码到DOM节点之间的转换过程

  • 我们能够控制的是JSX, 也就是ReactElement对象.
  • fiber树是通过ReactElement生成的, 如果脱离了ReactElement,fiber树也无从谈起. 所以是ReactElement树(不是严格的树结构, 为了方便也称为树)驱动fiber树.
  • fiber树是DOM树的数据模型, fiber树驱动DOM树

我们通过编程只能控制ReactElement树的结构, ReactElement树驱动fiber树, fiber树再驱动DOM树, 最后展现到页面上. 所以fiber树的构造过程, 实际上就是ReactElement对象到fiber对象的转换过程.

2、React Element 和 Fiber Node 结构

2.1 React Element 数据结构

rust 复制代码
const element = { 
    $$typeof: REACT_ELEMENT_TYPE,//用来标记这个对象是一个React元素。React使用这个属性来防止XSS攻击,并确保对象是由React创建的
    
  	// 内部属性
  	type: type,//div a 元素标签 
    key: key, // 就是那个key
    ref: ref,  // 就是你知道的ref
    props: props, 

    // ReactFiber 记录创建本对象的fiber节点, 还未与fiber关联之前,该属性为null
    __owner: null
}

2.2 Fiber 数据结构

kotlin 复制代码
// 创建FiberNode
export class FiberNode {
  tag: WorkType // tag 就是指什么类型的fiberNode
  key: Key  //元素的唯一标识。
  ref: Ref

  stateNode: any  // 对应的真实DOM
  type: any  // fiberNode的类型

  return: FiberNode | null // 父fiber
  child: FiberNode | null // 子fiber
  sibling: FiberNode | null // 兄弟fiber

  index: number

  pendingProps: Props
  memoizeProps: Props | null
  memoizedState: any
  UpdateQueue: unknown

  alternate: FiberNode | null // 用于fiberNode之间的切换

  flags: Flags
  subtreeFlags: Flags
  constructor(tag: WorkType, pendingProps: Props, key: Key) {
    // 对于fiberNode来说 是实例的属性
    this.tag = tag
    this.key = key

    // 对于HostComponent来说  如果是一个div的话 stateNode保存的就是div这个Dom
    this.stateNode = null
    // 对于一个FunctionComponent来说  type就是这个函数组件本身 () => {}
    this.type = null // fiberNode的类型

    // 表示节点之间的关系 构成树状结构
    /**
     * 为什么使用return表示父子关系 而不是用parent等其他字段?
     * 因为fiberNode作为一个工作单元,return指"FiberNode执行完了complete Work后返回的下一个父FiberNode"
     */
    this.return = null // 指向父FiberNode
    this.child = null
    this.sibling = null // 指向右边的兄弟fiberNode
    this.index = 0 // index是指如果同级的fiberNode有好几个, 比如<ul> <li> * 3 </ul>, 则第一个li的index是0 第二个li的index是1 第三个li的index是2

    this.ref = null

    // 作为工作单元
    this.pendingProps = pendingProps // pendingProps是指这个工作单元刚开始工作的时候,这个props是什么
    this.memoizeProps = null // memoizeProps 是指工作完了之后,这个工作单元fiberNode的props是什么
    this.memoizedState = null // memoizedState是指更新完成后的state baseState 1 update 2 -> memoizedState 2
    this.UpdateQueue = null  // 更新队列

    // wip 和 current的指针
    this.alternate = null
    // 副作用
    this.flags = NoFlags // 标记
    this.subtreeFlags = NoFlags // 子fiber所有的标记
  }
}

3、为什么不使用React Element这种数据结构 而使用fiber 结构

由第二章知道, JSX 方法执行后会返回一个新的 React 元素(ReactElement)。React 元素是一个轻量级的对象,描述了要渲染的 UI 组件的类型(type)、属性(props),和子元素(children)等信息,如果将React Element这种数据结构作为React Reconciler阶段的操作对象,会存在以下问题:

  1. 无法表达ReactElement节点与另一个ReactElement节点之间的关系(因为它只记录了自身的数据,比如组件的类型、属性和子元素等),一般把ReactElement称为React的数据存储单元;
  2. 字段有限,不好拓展(比如:无法表达状态)

当然在React 16版本之前,React 使用的是名为Stack Reconciler的旧调和算法。Stack Reconciler 的核心是递归遍历组件树,把数据保存在递归调用栈中。它使用的深层递归遍历方法。

但是使用递归遍历组件树时,会导致一些问题:

  1. 阻塞主线程:在 JavaScript 中,递归调用可能会阻塞主线程,因为 JavaScript 是单线程的。如果组件树很大或者更新很频繁,递归调用可能会导致 UI 变得不流畅,影响用户体验。
  2. 没有优先级调度:Stack Reconciler 无法对不同的更新任务进行优先级调度,所有的更新任务都会被视为相同的优先级。这意味着对于高优先级的任务(如动画或用户交互),React 无法优先处理,从而可能导致性能下降。

为了解决这些问题,React 引入了 Fiber Reconciler。Fiber Reconciler 使用了一种名为 "Fiber" 的新数据结构来表示组件树。

Fiber特点如下

  • 介于 ReactElement 与真实UI节点之间;
  • 能够表达节点之间的关系
  • 方便拓展,不仅作为数据存储单元,也能作为工作单元

4、Fiber Node的含义

FiberNode包含以下三层含义

  1. 作为架构,v15的Reconciler采用递归的方式执行,被称为 Stack Reconciler, v16之后的Reconciler是基于FiberNode实现的,被称为 Fiber Reconciler
  2. 作为静态数据结构,每个FiberNode对应一个React Element, 用于保存React Element的类型、对应的dom元素等信息
  3. 作为动态的工作单元,每个FiberNode用于保存"本次更新中该React Element的变化的数据、要执行的工作(插、删、改、更新ref、副作用等)

在表达fiber之间关系的时候,为什么采用return来指向父fiber,而不是用parent或者其他?

答:因为作为一个工作单元,return指的是"FiberNode执行完 completeWork后返回的下一个FiberNode",子FiberNode及其兄弟FiberNode执行完completeWork后会返回父FiberNode,所以使用return来表示父子FiberNode之间的关系

5、Fiber 工作原理

Fiber工作原理中最核心的点就是:可以中断和恢复,这个特性增强了React的并发性和响应性。

为了解决原来不可中断的渲染机制导致的卡顿、体验不佳等问题,引入Fiber架构,将原来耗时较长的更新任务,拆分成一个一个小任务,当任务时间超过执行时间的时候,将控制权交还给浏览器,这个也就是time slice,而实现这个机制,主要是在fiber 架构中,fiber node可以作为一个工作单元,拥有链接属性,表明各fiber node之间的关系,还有使用了双缓冲技术。

5.1 工作单元

每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。

5.2 链接属性

childsiblingreturn 字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。

5.3 双缓冲技术

由于 render 阶段构建 workInProgress 树的过程是可以中断的,同时,workInProgress 树最终又会在 commit 阶段渲染到浏览器页面上,这就决定了在 render 阶段,必须要保持浏览器页面不变直到 render 阶段完成。也就是说我们在 render 阶段需要保持 current tree 不变,然后用另一棵树来承载 workInProgress 树。为了实现这个目标,React 借鉴了双缓冲技术。

Fiber 双缓冲树包括一棵 current tree 和一棵 workInProgress tree(render 阶段完成后的 workInProgress 树也叫 finishedWork 树)。current tree 保存的是当前浏览器页面对应的 fiber 节点。workInProgress tree 是在 render 阶段,react 基于 current tree 和新的 element tree 进行比较而构建的一棵树,这棵树是在内存中构建,在 commit 阶段将被绘制到浏览器页面上。

直白的讲就是,你面前有2棵树,一棵你看见的,这棵树叫 current fiber tree,一棵你看不见的,这棵叫workInProgress Fiber Tree, 简称 wip fiber tree,

当reconciler结束的时候,你看不见的这颗树在内存中已经更新完成,会提交到React的commit阶段,完成渲染,在渲染之前,首先要将这两棵树的定位对调,也就是你原来看不见的那棵树(wip fiber tree)会变成你看得见的树(current fiber tree),这个就是双缓冲技术

因为在react的commit阶段, 是渲染的时候,所以这个时候是绝对不可能中断的,否则你将看到一个不完整的阶段,所以我们所说的可中断只能发生在React的render阶段,也就是reconciler的时候。

5.3.1 双缓存树切换规则

React应用的根节点通过current指针在不同Fiber树的HostRootFiber根节点(ReactDOM.render创建的根节点)间切换。

  • 在 mount时(首次渲染),会根据jsx方法返回的React Element构建Fiber对象,形成Fiber树;
    • 然后这棵Fiber树会作为current Fiber应用到真实DOM上
  • 在 update时(状态更新),会根据状态变更后的React Element和current Fiber作对比形成新的workInProgress Fiber树
    • 即当workInProgress Fiber树构建完成交给Renderer(渲染器)渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树
    • 然后workInProgress Fiber切换成current Fiber应用到真实DOM上,这就达到了更新的目的。

这一切都是在内存中发生的,从而减少了对DOM的直接操作。

每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新,这就是React中用的双缓存树切换规则

javascript 复制代码
function App() {
  const [elementType, setElementType] = useState('div');

  const handleClick = () => {
    setElementType(prevElementType => {
      return prevElementType === 'div' ? 'p' : 'div';
    })
  }

  // 根据 elementType 的值动态创建对应的元素
  const Element = elementType;

  return (
    <div>
      <Element onClick={handleClick}>
        点击我切换 div 和 p 标签
      </Element>
    </div>
  )
}

const root = document.querySelector("#root");
ReactDOM.createRoot(root).render(<App />);

6、Fiber Tree的构建过程

由前面知道,React是通过JSX描述页面结构

javascript 复制代码
function App () {
  return <div>
    hello world
    <span>jsx是什么</span>
  </div>
}
php 复制代码
var _reactJsxRuntime = require("react/jsx-runtime");
function App() {
  return /*#__PURE__*/ _reactJsxRuntime.jsxs("div", {
    children: [
      "hello world",
      /*#__PURE__*/ _reactJsxRuntime.jsx("span", {
        children: "jsx\u662F\u4EC0\u4E48"
      })
    ]
  });
}

为什么使用Symbol.for而不是使用Symbol?

1 唯一性:Symbol 创建的标识符是全局唯一的,每个 Symbol 值都是不同的,而且无法通过其他方式访问到。而 Symbol.for() 方法在全局符号注册表中维护一个符号的键-值对映射,如果已存在具有给定键的符号,就返回该符号,否则创建一个新的符号并存储在注册表中。

2 全局可访问性:通过 Symbol 创建的符号只能在局部作用域中使用,无法在不同的上下文中共享。而使用 Symbol.for() 创建的符号可以全局共享,可以在不同的上下文中通过相同的键获取到相同的符号。

ini 复制代码
// 使用 Symbol 创建标识符
const symbol1 = Symbol('symbol');
const symbol2 = Symbol('symbol');
console.log(symbol1 === symbol2);  // false,每个 Symbol 值都是唯一的

// 使用 Symbol.for 创建和访问符号
const symbol3 = Symbol.for('shared');
const symbol4 = Symbol.for('shared');
console.log(symbol3 === symbol4);  // true,在全局注册表中共享了相同的符号

// 通过 Symbol.keyFor 获取 Symbol.for 创建的符号的键
const key = Symbol.keyFor(symbol3);
console.log(key);  // 'shared'

也就是说 createElement方法或者JSX方法执行的结果就是VDOM,也就是 React Element的实例,ReactElement元素

在16之前,就直接拿着这个层级很深的对象进行递归渲染,所以这个过程是无法中断的。

在16及以后,做的改变就是不是直接渲染这个vdom,而是先转成fiber tree,采用这种fiber架构来实现异步可中断,每一个fiber node就是一个工作单元

6.1 ReactDOM.createRoot().root()做了什么

javascript 复制代码
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<App />)

由上面代码,createRoot方法接受一个dom元素作为参数

所以 ReactDom.createRoot().render() 就创建了我们整个应用的唯一的根节点 FiberRootNode和HostFIberNode, 并且将FiberRootNode的current指针指向了HostFiberNode,HostFiberNode就是div#root所对应的fiberNode

在执行render方法内部会去执行updateContainer方法

6.2 updateContainer()方法

javascript 复制代码
// children就是我们传入的<App />,即通过jsx编译后的element结构
ReactDOMRoot.prototype.render = function(children: ReactNodeList) {
  const root = this._internalRoot; // FiberRootNode
  updateContainer(children, root, null, null);
};

入口函数render()传入的参数,就是的ReactElement结构,通过_internalRoot得到整个应用的根节点,也就是FiberRootNode

ini 复制代码
/**
 * 将element结构转为fiber树
 * @param {ReactNodeList} element 虚拟DOM树
 * @param {OpaqueRoot} container FiberRootNode 节点
 * @param {?React$Component<any, any>} parentComponent 在React18传到这里的是null
 * @param {?Function} callback render()里的callback,不过从React18开始就没了,传入的是null
 * @returns {Lane}
 */
// 去除 dev 代码和任务优先级的调度
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  /**
   * current:
   * const uninitializedFiber = createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride,);
   */
  // FiberRootNode.current 现在指向到当前的fiber树,
  // 若是初次执行时,current树只有hostFiber节点,没有其他的
  const current = container.current;
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(current);

  // 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
  /**
   * const update: Update<*> = {
   *   eventTime,
   *   lane,
   *   tag: UpdateState,
   *   payload: null,
   *   callback: null,
   *   next: null,
   * };
   * @type {Update<*>}
   */
  const update = createUpdate(eventTime, lane);
  update.payload = { element };

  // 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
  // 不过从React18开始,render不再传入callback了,即这里的if就不会再执行了
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  /**
   * 将update添加到current的更新链表中
   * 执行后,得到的是 current.updateQueue.shared.pending = sharedQueue
   * sharedQueue是React中经典的循环链表,
   * 将下面的update节点插入这个shareQueue的循环链表中,pending指针指向到最后插入的那个节点上
   */
  enqueueUpdate(current, update, lane);
  /**
   * 这里传入的current是HostRootFiber的fiber节点了,虽然他的下面没有其他fiber子节点,
   * 但它的updateQueue上有element结构,可以用来构建fiber节点
   * 即 current.updateQueue.shared.pending = sharedQueue,element结构在sharedQueue其中的一个update节点,
   * 其实这里只有一个update节点
   */
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    entangleTransitions(root, current, lane);
  }
  return lane;
}

总结:

1、updateContainer(element, container)传入了两个参数,element 就是 jsx 编译后的 element 结构,而 container 表示的是 FiberRootNode,整个应用的根节点,并不是 DOM 元素;

2、container.current 指向的就是目前唯一的一棵 fiber 树的根节点,并 current 变量存储该节点,也就是HostFiberNode;

3、将 element 结构放到 current 节点的属性中,方便后续的构建:current.updateQueue.shared.pending = [{payload:{element}}];pending 是一个环形链表,element 就放在这个环形链表的节点中,在初始更新阶段,只有这一个 update 节点;

4、调用 scheduleUpdateOnFiber(current);该方法内部将 element 取出,构建出下一个 fiber 节点

6.3 scheduleUpdateOnFiber()方法

由函数名称也能看出来,这里主要是在做和调度更新优先级相关的事情,忽略代码直接进入ensureRootIsScheduled()方法

这里的root就是指整个应用的根节点 FiberRootNode

这里也是一堆的调度,继续找,在ensureRootIsScheduled内部会调用一个performConcurrentWorkOnRoot.bind(null, root),继续往下找,在performConcurrentWorkOnRoot.bind(null, root)方法内会去调用renderRootConcurrent()

6.4 renderRootConcurrent()方法

这里的 root 是整个应用的根节点,即 FiberRootNode,会传入到函数 prepareFreshStack() 中,主要是为了接下来的递归,初始化一些数据和属性

markdown 复制代码
/**
 * 整个应用目前只有 FiberRootNode和current两个节点,current树只有一个根节点,就是current自己;
 * prepareFreshStack() 函数的作用,就是通过current树的根节点创建出另一棵树的根节点,
 * 并将这两棵树通过 alternate 属性,实现互相的指引
 * workInProgressRoot: 是将要构建的树的根节点,初始时为null,经过下面 prepareFreshStack() 后,
 * root.current给到workInProgressRoot,
 * 即使第二次调用了,这里的if逻辑也是不会走的
 * workInProgress初始指向到workInProgressRoot,随着构建的深入,workInProgress一步步往下走
 */
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
  /**
   * 将整个应用的根节点和将要更新的fiber树的根节点赋值到全局变量中
   * root是当前整个应用的根节点
   */
  prepareFreshStack(root, lanes);
}
ini 复制代码
/**
 * 准备新堆栈,返回「更新树」的根节点
 * @param root
 * @param lanes
 * @returns {Fiber}
 */
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  workInProgressRoot = root; // 整个React应用的根节点,即 FiberRootNode

  /**
   * prepareFreshStack()个人认为是只在初始化时执行一次,root是整个应用的根节点,而root.current就是默认展示的那棵树,
   * 在初始化时,current 树其实也没内容,只有这棵树的一个根节点;
   * 然后利用current的根节点通过 createWorkInProgress()方法 创建另一棵树的根节点rootWorkInProgress
   * createWorkInProgress()方法内则判断了 current.alternate 是否为空,来决定是否可以复用这个节点,
   * 在render()第一次调用时,root.current.alternate 肯定为空,这里面则会调用createFiber进行创建
   */
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  // 初始执行时,workInProgress指向到更新树的根节点,
  // 在mount阶段,workInProgress是新创建出来的,与current树的根节点workInProgressRoot,肯定是不相等的
  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;

  return rootWorkInProgress;
}

总结:

  1. 将整棵树的根节点 root 给到 workInProgressRoot;
  2. createWorkInProgress() 利用 current 节点创建出「更新树」的根节点;createWorkInProgress函数内部大致执行内容就是判断 current.alternate (即 current 互相对应的那个节点)是否为空,若为空则创建出一个新节点;若不为空,则直接复用之前的节点,然后将新特性给到这个节点(不过我们这里传入的是 null)
  3. workInProgress 指针初始指向到「更新树」的根节点,在接下来的递归操作中,该指针一直在变动

回到renderRootConcurrent()方法,进入到workLoopConcurrent()方法

6.5 workLoopConcurrent() 方法

react 2个 循环

1:任务调度循环

2:fiber构造循环

shouldYield就是用来决定是否将控制权交还给浏览器

就是一个 while 循环,每次循环时,都会执行函数 performUnitOfWork() ,然后操作 workInProgress 指向的那个 fiber 节点,直到 workInProgress 为 null。刚才在上面的 prepareFreshStack() 中,workInProgress 指针指向到了「更新树」的根节点 rootWorkInProgress(即跟 current 树根节点长得一样的那个节点),这个 fiber 节点里的 updateQueue.shared.pending 中的一个 update 里,存放着 element 结构。

同步更新流程回去执行workLoopSync, 并发更新流程会执行workLoopConcurrent

注释的意思翻译过来大白话应该是:

并发:在调度器不要求让出或者wip不为null,循环就不停止

同步:只要开始执行,能停止循环的前提只有一个,那就是wip为null了

6.6 performUnitOfWork()方法

从这里开始,就进入了reconciler阶段

所谓的fiber 构造循环也就是在这个阶段

循环以DFS递归的方式进行遍历

一个是递的阶段: 也就对应 beginWork()

一个是归的阶段:也就是对应completeWork()

两个方法交替执行

而每个阶段又细分为mount和update场景

ini 复制代码
function performUnitOfWork(unitOfWork: Fiber): void {
  /**
   * 初始mount节点时,unitOfWork 是上面workLoopConcurrent()中传入的 workInProgress,
   * unitOfWork.alternate 指向的是 current
   */
  const current = unitOfWork.alternate;

  let next;
  /**
   * current为当前树的那个fiber节点
   * unitOfWork为 更新树 的那个fiber节点
   * 在初始mount节点,current和unitOfWork都是fiberRoot节点
   * 在第一次调用beginWork()时,element结构通过其一系列的流程,创建出了第一个fiber节点,即<App />对应的fiber节点(我们假设<App /> 是最外层的元素)
   * next就是第一个fiber节点,然后next给到workInProgress,接着下一个循环
   */
  next = beginWork(current, unitOfWork, subtreeRenderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // unitOfWork已经是最内层的节点了,没有子节点了
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

将当前 current 节点和更新树的节点,都传给 beginWork(current, unitOfWork) 函数。简单来说,beginWork 会根据当前 fiber 节点中的 element 结构,创建出新的 子fiber 节点,workInProgress 再指向到这个新 fiber 节点继续操作,直到所有的数据都操作完。

这里我们只需要知道,诸如函数组件、类组件也是 fiber 节点,也是整棵 fiber 树的一部分。其内部的 jsx(element)再继续转为 fiber 节点。

若 beginWork()返回的 next 是 null,说明当前节点 workInProgress 已经是最内层的节点了,就会进入到函数 completeUnitOfWork() 中。可以看到执行的流程是深度优先,即若当前 fiber 还能构造出子节点,即一直向下构造。直到没有子节点后,才会流转到兄弟节点和父级节点。

6.7 completeUnitOfWork()方法

这里就对应归的阶段,当前节点和当前所有的子节点都执行完了,就会调用该方法。

csharp 复制代码
/**
 * 当前 unitOfWork 已没有子节点了
 * 1. 若还有兄弟节点,将 workInProgress 指向到其兄弟节点,继续beginWork()的执行;
 * 2. 若所有的兄弟节点都处理完了(或者没有兄弟节点),就指向到其父级fiber节点;回到1;
 * 3. 直到整个应用根节点的父级(根应用没有父级节点,所以为null),才结束;
 */
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return; // 每个节点都有一个return属性,指向到其父级节点

    // 若有兄弟节点,则继续执行兄弟节点
    const siblingFiber = completedWork.sibling; // 该节点下一个兄弟节点
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // 当前节点和兄弟节点全部遍历完毕,则返回到其父节点
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null); // while的作用就是若该节点没有兄弟节点,能够一直往上找父级节点,
}

6.8 总结

构建fiber tree的整个流程:

从最开始执行ReactDOM.reactRoot().render()方法开始,首先会创建整个应用的FIberRootNode以及HostRootFiber,然后在render方法内部会调用一系列的方法,比如updateContainer(),schedultUpdateOnFiber(),renderRootConcurrent()等方法,然后在wookLoopConcurrent内 依赖ReactElement开始构建整个fiber Tree, 整个构建过程是以DFS的方式遍历循环,分为递和归两个阶段,在递的阶段去执行beginWork(),由上至下去构建fiber Node, 如果到了最底层,则转向兄弟节点或者父节点,在归的阶段去执行completeWork,直到所有的节点遍历结束,循环也就停止了,workInProgress也指向了null,此时一颗 wip fiber tree 已经构建完成,接下来就进入到React的commit阶段。

7、React是如何实现增量渲染的

React 实现增量渲染是通过 Fiber 架构和协调器来实现的。增量渲染是一种渐进式渲染方式,它将整个渲染过程拆分成多个增量步骤,并将这些步骤分布在多个时间片段中执行,从而实现渲染的分阶段、增量式进行。

通过时间分片的方式将整个渲染过程分成多个时间片段。这样页面的渲染过程被分成多个增量步骤进行,避免了长时间的渲染阻塞。React 使用虚拟 DOM 和 Diff 算法来对比前后两个状态的差异,然后仅更新真正需要变化的部分,而不是重新渲染整个组件树。这样,React 只需要更新变化的部分,从而实现增量渲染。

8、fiber是如何实现中断和恢复渲染的

在协调阶段,每一个fiber node都是一个工作单元,fiber node会记录该节点的相关信息,同时拥有 return、child、sibling来指向父、子、兄弟节点,形成一个链表结构的fiber tree,在中断渲染的时候,react会记录当前中断位置,将控制权交换给浏览器。每个fiber node会有一个alternate属性,它指向对应的fiber node,当前这个正被处理的被成为workInprogress,每处理完一个节点,会检查时间片是否到时,如果到时,则会中断,当中断时,会将该Fiber节点的"alternate"属性指向一个新创建的Fiber节点,用于记录中断时的位置信息,在下次更新任务,协调器会从记录的中断位置开始继续更新。

这里感觉总结的还是不太好,有没有大佬帮忙总结一下

相关推荐
莹雨潇潇11 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr19 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀4 小时前
CSS——属性值计算
前端·css
DOKE4 小时前
VSCode终端:提升命令行使用体验
前端