提交 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;
    }
  }
}
相关推荐
拾光拾趣录9 分钟前
给Electron-Claude应用构建全面的数据统计体系 - 从0到1的实践总结
前端·electron
拾光拾趣录36 分钟前
8道题穿透前端原理层
前端·面试
cc蒲公英1 小时前
uniapp x swiper/image组件mode=“aspectFit“ 图片有的闪现后黑屏
java·前端·uni-app
前端小咸鱼一条1 小时前
React的介绍和特点
前端·react.js·前端框架
谢尔登1 小时前
【React】fiber 架构
前端·react.js·架构
哈哈哈哈哈哈哈哈8531 小时前
Vue3 的 setup 与 emit:深入理解 Composition API 的核心机制
前端
漫天星梦1 小时前
Vue2项目搭建(Layout布局、全局样式、VueX、Vue Router、axios封装)
前端·vue.js
ytttr8732 小时前
5G毫米波射频前端设计:从GaN功放到混合信号集成方案
前端·5g·生成对抗网络
水鳜鱼肥2 小时前
Github Spark 革新应用,重构未来
前端·人工智能
前端李二牛2 小时前
现代CSS属性兼容性问题及解决方案
前端·css