提交 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;
    }
  }
}
相关推荐
前端工作日常3 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一3 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华3 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言3 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选3 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD3 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318553 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce4 小时前
记一次微信小程序分包经历
前端
LeeAt4 小时前
从Promise到async/await的逻辑演进
前端·javascript
等一个晴天丶4 小时前
不一样的 TypeScript 入门手册
前端