Fiber 架构

目标虚拟 DOM

js 复制代码
const element = (
  <h1>
    hello <span>test</span> children
  </h1>
);

针对以上代码的虚拟DOM就不在这里再列出来了。

架构分析

createRoot 函数中,已经实现了 HostRootFiberFiberRootNode。接下来需要开始构建 Fiber 树。先上图。

在上图中,出现了一些属性,这里需要着重介绍。

双缓存

在上图中,出现了被方框分隔的两部分,他们都是 Fiber 树。在源代码中,分别称为 currentworkInProgress

  • current: 正在屏幕上显示的 Fiber 树。
  • workInProgress: 正在构建、即将替换 current 的那棵树。

这是为了高效比较前后两次更新,并且可中断和批量一次性提交。

currentworkInProgress 之间通过 alternate 属性互相指向,当 workInProgress 完成并且提交后,workInProgress 会成为 currentcurrent 会转为 workInProgress,实现复用。

Fiber 树

Fiber 树的关键元素有这几个:

  • child: 当前 Fiber 节点的大儿子
  • return: 当前 Fiber 节点的父亲
  • sibling: 当前 Fiber 节点的弟弟

Fiber 树是根据虚拟DOM 树构建而来。通过以上三个属性构建出 Fiber 树。

Fiber 树叫树是因为长得像树,但是实际的数据结构是链表,可以很方便的中断链表任务。

每个 Fiber 节点会被遍历两次,第一次根据虚拟DOM构建 Fiber 树,第二次完成构建。

代码实现

代码逻辑从 renderRootSync 函数开始看,忽略之前一些调用函数。

renderRootSync 复制代码
function renderRootSync(root) {
  // 开始构建 fiber 树
  prepareFreshStack(root);
  workLoopSync();
}

可以看到 renderRootSync 函数中调用了两个函数。 prepareFreshStackworkLoopSync 函数。

prepareFreshStack 复制代码
function prepareFreshStack(root) {
  // 创建 workInProgress
  workInProgress = createWorkInProgress(root.current, null);
}

这里的 createWorkInProgress 的作用就是创建 workInProgress,逻辑是当 current.alternate 不存在时创建一个新的 Fiber,存在时则复用。

重点看 workLoopSync 函数。

workLoopSync 复制代码
function workLoopSync() {
  while (workInProgress) {
    performUnitOfWork(workInProgress);
  }
  return workInProgress;
}

这个函数其实很简单,就是调用了 performUnitOfWork 函数。

performUnitOfWork 复制代码
function performUnitOfWork(unitOfWork) {
  // 获取新 fiber 对应的老 fiber
  const current = unitOfWork.alternate;
  // 完成当前 fiber 的子 fiber 链表构建后,返回 child
  const next = beginWork(current, unitOfWork);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 没有子节点,则已经完成
    completeUnitOfWork(unitOfWork);
  } else {
    // 存在子节点,则继续构建
    workInProgress = next;
  }
}

如果忽略 performUnitOfWork 中的 completeUnitOfWork 逻辑,那么这就是一个经典的链表遍历,通过 workInProgressnext 变量缓存 child 节点,一直到没有子节点退出。

beginWork 复制代码
export function beginWork(current, workInProgress) {
  switch (workInProgress.tag) {
    // 处理根节点
    case HostRoot:
      return updateHostRoot(current, workInProgress);
    // 处理原生节点
    case HostComponent:
      return updateHostComponent(current, workInProgress);
    // 处理文本节点
    case HostText:
      return null;
    default:
      return null;
  }
}

beginWork 中,对 Fiber 节点进行区别,不同类型的节点执行不同的处理函数。

updateHostRoot 复制代码
function updateHostRoot(current, workInProgress) {
  // 虚拟DOM信息在更新队列中
  processUpdateQueue(workInProgress);
  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element;
  // 根据新的虚拟DOM 生成新的 fiber 链表
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

updateHostRoot 函数中,首先要处理的是更新队列,这里需要知道,虚拟DOM 信息在一开始初始化更新队列的时候就放到了根节点的更新队列中,所以这个处理逻辑就很清楚,就是将更新队列中的所有更新合并,并返回,然后赋值到 workInProgress.memoizedState。对于根节点来说,nextChildren 就是 memoizedState 中的 element 元素。再执行 reconcileChildren 函数,这个函数的作用是根据虚拟DOM,生成 Fiber 链表。最后返回 workInProgress.child

processUpdateQueue 复制代码
const queue = workInProgress.updateQueue;
  const pendingQueue = queue.shared.pending;
  // 如果有更新
  if (pendingQueue !== null) {
    // 清除等待生效的更新
    queue.shared.pending = null;
    // 获取队列中最后一个更新
    const lastPendingUpdate = pendingQueue;
    // 获取队列中第一个更新
    const firstPendingUpdate = lastPendingUpdate.next;
    // 解开循环链表
    lastPendingUpdate.next = null;
    // 获取老状态
    let newState = workInProgress.memoizedState;
    let update = firstPendingUpdate;
    while (update) {
      // 根据老状态和更新,计算新状态
      newState = getStateFromUpdate(update, newState);
      update = update.next;
    }
    // 把最终计算的状态赋值
    workInProgress.memoizedState = newState;
reconcileChildren 复制代码
function reconcileChildren(current, workInProgress, nextChildren) {
  // 如果新 fiber 没有老 fiber  则说明是挂载
  if (current === null) {
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren);
  } else {
    // 如果有老老 fiber 则说明是更新
    // DOM-DIFF 拿老 fiber 和新的虚拟DOM进行对比,进行更新
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren);
  }
}
arduino 复制代码
// 没有老 fiber,走这个方法挂载
export const mountChildFibers = createChildReconciler(false);
// 存在老 fiber,走这个方法更新
export const reconcileChildFibers = createChildReconciler(true);

对于 createChildReconciler 函数就不细看了,它的工作主要有以下几点:

  • 生成子 Fiber 节点并挂载到父节点(单节点和多节点)
  • 标记是否操作标识(flags) 在 createChildReconciler 函数执行完成后,返回子节点,如果子节点为空,则进入 completeUnitOfWork 函数。
completeUnitOfWork 复制代码
function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    // 执行完成逻辑
    completeWork(current, completedWork);
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

completeUnitOfWork 函数是先遍历所有兄弟节点,如果没有兄弟节点,则回到父节点。是自底向上的遍历。具体代码也就不看了,注意功能就是根据不同类型的 Fiber 节点创建不同的真实DOM。并将操作标记向上冒泡。

结束

以上就是第一次挂载节点的执行流程。

相关推荐
小小小小宇4 分钟前
前端canvas手动实现复杂动画示例
前端
codingandsleeping6 分钟前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇30 分钟前
前端PerformanceObserver使用
前端
zhangxingchao2 小时前
Flutter中的页面跳转
前端
烛阴2 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝3 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇3 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军3 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加4 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam5 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript