提交 Fiber 树

在完成 Fiber 树的渲染后,需要提交 Fiber 树,将 Fiber 树对应的真实DOM渲染到页面上,即挂载到根节点上。

实现

performConcurrentWorkOnRoot 复制代码
function performConcurrentWorkOnRoot(root) {
  // 以同步方式渲染,第一次渲染都是同步
  renderRootSync(root);
  // 开始进入提交阶段,就是执行副作用,修改真实DOM
  const finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  commitRoot(root);
}

renderRootSync 函数执行完成后,Fiber 树的处理也就完成了,这个时候有两个 Fiber 树,一个是只有一个根 Fiber 的空树,另一个是根据虚拟DOM渲染出的 Fiber 树。

commitRoot 复制代码
function commitRoot(root) {
  const { finishedWork } = root;
  // 判断当前节点的子树是否有副作用
  const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
  // 根节点是否有副作用
  const rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags;
  // 有副作用则提交
  if (subtreeHasEffects | rootHasEffects) {
    commitMutationEffectsOnFiber(finishedWork, root);
  }
  // 等 DOM 变更后,指向完成的 Fiber 树
  root.current = finishedWork;
}
ini 复制代码
export const MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;

在真正执行提交逻辑之前,需要判断当前 Fiber 节点或者当前 Fiber 节点的子 Fiber 是否存在副作用,有副作用再执行 commitMutationEffectsOnFiber 函数。MutationMask 是多种副作用的或计算。 改变 FiberRootNodecurrent 的指向,让它指向提交完成后的 Fiber 树,即 finishedWork

commitMutationEffectsOnFiber 复制代码
// 暂时只处理原生节点
export function commitMutationEffectsOnFiber(finishedWork, root) {
  switch (finishedWork.tag) {
    case HostRoot:
    case HostComponent:
    case HostText: {
      // 递归遍历,先处理子节点的副作用
      recursivelyTraverseMutationEffects(finishedWork, root);
      // 再处理自己的副作用
      commitReconciliationEffects(finishedWork);
    }
  }
}
function recursivelyTraverseMutationEffects(parentFiber, root) {
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child) {
      commitMutationEffectsOnFiber(child, root);
      child = child.sibling;
    }
  }
}

可以看到,对子节点的处理其实就是一个递归遍历,找到最下面的子节点,然后调用 commitReconciliationEffects 函数进行处理。

commitReconciliationEffects 复制代码
function commitReconciliationEffects(finishedWork) {
  const { flags } = finishedWork;
  if (flags & Placement) {
    // 插入
    commitPlacement(finishedWork);
    // 删除 Placement
    finishedWork.flags = flags & ~Placement;
  }
}

这里的逻辑页非常简单,判断副作用类型为插入,然后调用插入函数 commitPlacement 最后删除副作用标记。

commitPlacement 实现

对于插入操作,再根据 Fiber 节点的定义,可以很容易的想到。通过当前 Fiber节点的 return 属性获取父 Fiber 节点,在根据 stateNode 属性获取父 Fiber 节点对应的真实DOM,最后一插入,就搞定了。大概像这样。

js 复制代码
function commitPlacement(finishedWork) {
  const parentFiber = finishedWork.return
  switch (parentFiber.tag) {
    case HostRoot: {
      const parent = parentFiber.stateNode.containerInfo;
      parent.appendChild(parent, finishedWork.stateNode)
      break;
    }

    case HostComponent: {
      const parent = parentFiber.stateNode;
      // 同上
    }
  }
}

但是对于下面图里这个 Fiber 树结构来说,这就不适用了。

通过这个图可以发现,父 Fiber 有可能是一个函数式组件,并没有对应的真实DOM。所以第一步,需要找到当前 Fiber 节点对应的真实DOM 节点。

js 复制代码
function getHostParentFiber(fiber) {
  let parent = fiber.return;
  while (parent) {
    if (isHostParent(parent)) {
      return parent;
    }
    parent = parent.return;
  }
  return parent;
}

这个代码很简单,通过 while 循环一直往上找,直到父 Fiber 是一个真实DOM类型的 Fiber节点,或者不存在。 在找到真实父 Fiber 节点后,其实还是不可以直接插入,使用 append 会直接将子节点插入到父节点的最后,如果父节点的子节点不为空,那么会造成顺序的混乱,所以还需要找到对应的真实兄弟节点。 这里有几个点需要注意:

  1. 这个真实节点的副作用标识一定是更新。因为只有更新才代表这个节点在之前已经存在。如果是插入,表示这个节点还没有创建,不能插入。
  2. 如果当前节点没有兄弟,且父元素不为真实DOM类型 Fiber 节点,那么可以查看父节点的兄弟节点,不断循环,如果父节点为真实DOM 类型,则退出循环,直接插入。

图中蓝色箭头为执行顺序。

  1. 首先判断 Host1 是否存在兄弟节点,不存在则找父亲节点。
  2. 一直向上找,直到找到节点存在兄弟节点。
  3. 判断兄弟节点子节点,找到操作类型为更新的节点,返回。
js 复制代码
function getHostSibling(fiber) {
  let node = fiber;
  sibling: while (true) {
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        return null;
      }
      node = node.return;
    }
    node = node.sibling;
    while (node.tag !== HostComponent && node.tag !== HostText) {
      if (node.flags & Placement) {
        continue sibling;
      } else {
        node = node.child;
      }
    }
    if (!(node.flags & Placement)) {
      return node.stateNode;
    }
  }
}

完整的 commitPlacement 函数。

js 复制代码
function commitPlacement(finishedWork) {
  const parentFiber = getHostParentFiber(finishedWork);
  switch (parentFiber.tag) {
    case HostRoot: {
      const parent = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNode(parent, before, finishedWork);
      break;
    }

    case HostComponent: {
      const parent = parentFiber.stateNode;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNode(parent, before, finishedWork);
      break;
    }
  }
}
相关推荐
洛小豆4 分钟前
深入理解Pinia:Options API vs Composition API两种Store定义方式完全指南
前端·javascript·vue.js
洛小豆14 分钟前
JavaScript 对象属性访问的那些坑:她问我为什么用 result.id 而不是 result['id']?我说我不知道...
前端·javascript·vue.js
叹一曲当时只道是寻常17 分钟前
Softhub软件下载站实战开发(十六):仪表盘前端设计与实现
前端·golang
超级土豆粉22 分钟前
npm 包 scheduler 介绍
前端·npm·node.js
bug爱好者23 分钟前
原生小程序如何实现跨页面传值
前端·javascript
随笔记26 分钟前
uniapp开发的小程序输入框在ios自动填充密码,如何欺骗苹果手机不让自动填充
前端·ios·app
bug爱好者32 分钟前
原生微信小程序最实用的工具函数合集
前端·javascript
3Katrina35 分钟前
JS事件机制详解(2)--- 委托机制、事件应用
前端·javascript·面试
Allen Bright40 分钟前
【CSS-15】深入理解CSS transition-duration:掌握过渡动画的时长控制
前端·css
张鑫旭41 分钟前
40岁老前端2025年上半年都学了什么?
前端