提交 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;
    }
  }
}
相关推荐
南风知我意9577 分钟前
【前端面试3】初中级难度
前端·javascript·面试
霍理迪11 分钟前
JS作用域与预解析
开发语言·前端·javascript
切糕师学AI15 分钟前
.NET Core Web 中的健康检查端点(Health Check Endpoint)
前端·kubernetes·.netcore
蓉妹妹20 分钟前
在React中使用Scroll嵌套Scroll,出现里面Scroll滚动条超出高度却滚动没反应的问题,解决方案添加nestedScrollEnabled
javascript·react native·react.js
rosmis35 分钟前
地铁病害检测系统软件改进记录-2-02
开发语言·前端·javascript
css趣多多39 分钟前
解决ui组件flex1容器底部被撑开的问题
前端
乔江seven1 小时前
【python轻量级Web框架 Flask 】2 构建稳健 API:集成 MySQL 参数化查询与 DBUtils 连接池
前端·python·mysql·flask·web
Alaaaaaaan1 小时前
[DevOps]使用github-action工具部署docker容器(实现提交代码一键推送部署到服务器)
服务器·前端·docker·容器·github
摘星编程1 小时前
在OpenHarmony上用React Native:Spinner自定义样式
javascript·react native·react.js
摇滚侠1 小时前
css 设置边框
前端·css