React 源码揭秘 | commit流程

前面文章所描述的都发生在render过程中。React包含两个过程,即render和commit过程,其中render过程是可以打断的,而commit阶段是不可打断的。

commit阶段可以理解是真正的操作DOM的阶段,其消费render阶段打到Fiber节点上的Flag,并且根据Flag对真实DOM元素进行更新,删除,插入等操作。

先来复习一下Flag,我们知道,commit包含两个子阶段,即:

Mutation阶段: 此阶段主要进行DOM的更新操作,包含DOM的增删改,以及Ref的更新阶段。次阶段也包涵Effect的收集。

Passive阶段: 此阶段是在Mutation阶段完成之后,进行一些副作用的处理阶段,一般在浏览器进行完一个循环的渲染之后执行。但是由于Effect是交给scheduler执行的,如果浏览器在完成Mutation阶段之后还有空闲时间,也有可能先执行useEffect。

对于Muation阶段,React主要处理以下Flags

TypeScript 复制代码
export const Placement = 0b0000001;
export const Update = 0b0000010;
export const ChildDeletion = 0b0000100;
export const Ref = 0b0010000;

export const MutationMask =
	Placement | Update | ChildDeletion | Ref

可以看到,Mutation阶段主要处理 dom元素插入 删除 更新 以及Ref的更新,MutationMask就是这四个Flag的合集。

Passive阶段,主要处理PassiveEffect和ChildDeletion

TypeScript 复制代码
export const PassiveEffect = 0b0001000;
export const PassiveMask = PassiveEffect | ChildDeletion;

因为useEffect不仅仅是在监听值变动时执行,在组件卸载时,也会执行其返回的函数

PassiveMask为这两个Flags的合集

commit准备工作

在render阶段结束之后,此时已经有一棵完整的待更新Fiber树了。

此时需要为commitRoot做一些初始化工作

  1. root.finishedWork用来记录已经生成的待Commit的Fiber树,由于render阶段结束之后,workInProgress已经为空了,这个值需要用root.current.alternate获得。

  2. root.finishedLane 用来记录当前处理的lane,需要用wipRootRenderLane赋予

在给这两个属性赋值后,需要吧wipRenderLane置为NoLane

最后调用commitRoot函数,开始commit阶段

TypeScript 复制代码
// performWorkOnRoot
//任务完成 收尾 commit
// 设置root.finishedWork
root.finishedWork = root.current.alternate;
root.finishedLane = lane;
// 设置wipRootRenderLane = NoLane;
wipRootRenderLane = NoLane;
// 进入commit阶段
commitRoot(root);

commitRoot - 开启commit流程

commitRoot函数用来开启commit阶段,其要做的事情为:

  1. 拿到root.finishedWork & root.finishedLane 并且把这两个值置为空, 并且调用markRootFinished把当前处理的lane从root.pendingLanes去除,表示已经完成本lane的处理。

  2. 检测finishedWork根节点是否包含PassiveEffect或者其子节点subtreeFlasg是否包含PassiveMask, 如果包含,则开启Passive阶段,并且将Passive阶段处理函数推入scheduler,在Mutation阶段结束之后调度运行。

  3. 检测finishedWork的flags和subTreeFlags是否包含 (PassiveMask | MutationMask) 因为Mutation阶段除了要处理阶段的插入删除更新Ref,还需要收集Effect。

  4. 如果包含Flag,说明需要commit 开启commitMuationEffect

  5. 把root.current赋finishedWork

  6. 开启Layout阶段,调用commitLayoutEffect 用来挂载Ref

  7. 重新开启调度流程,更新结束

实现如下:

TypeScript 复制代码
/** commit阶段 */
export function commitRoot(root: FiberRootNode) {
  const finishedWork = root.finishedWork;

  if (finishedWork === null) return;

  const lane = root.finishedLane;
  root.finishedWork = null;
  root.finishedLane = NoLane;

  // 从root.pendingLanes去掉当前的lane
  markRootFinished(root, lane);

  /** 设置调度 执行passiveEffect */
  /** 真正执行会在commit之后 不影响渲染 */
  /** commit阶段会收集effect到root.pendingPassiveEffect */
  // 有删除 或者收集到Passive 都运行
  if (
    (finishedWork.flags & PassiveMask) !== NoFlags ||
    (finishedWork.subTreeFlags & PassiveMask) !== NoFlags
  ) {
    // 调度副作用
    scheduler.scheduleCallback(
      PriorityLevel.NORMAL_PRIORITY,
      flushPassiveEffect.bind(null, root.pendingPassiveEffects)
    );
  }

  /** hostRootFiber是否有effect  */
  const hostRootFiberHasEffect =
    (finishedWork.flags & (MutationMask | PassiveMask)) !== NoFlags;

  /** hostRootFiber的子树是否有effect  */
  const subtreeHasEffect =
    (finishedWork.subTreeFlags & (MutationMask | PassiveMask)) !== NoFlags;

  /** 有Effect才处理 */
  if (hostRootFiberHasEffect || subtreeHasEffect) {
    commitMutationEffects(finishedWork, root);
  }
  // commit完成 修改current指向新的树
  root.current = finishedWork;
  // commitLayout阶段 处理Attach Ref
  commitLayoutEffects(finishedWork, root);
  // 确保可以继续调度
  ensureRootIsScheduled(root);
}

commitMutationEffect - Mutation阶段

commitMutationEffect是高阶函数commitEffect返回的函数,其调用如下:

TypeScript 复制代码
export const commitMutationEffects = commitEffect(
  "mutation",
  MutationMask | PassiveMask,
  commitMutationEffectsOnFiber
);

其中commitEffect是个高阶函数,需要传入

  1. 当前commit的阶段,mutation|layout,

  2. 需要检测的Mask

  3. 处理函数callback

这个函数的作用就是返回一个处理函数,这个处理函数会深度优先遍历传入的finsihedWork 树,在向下递的阶段,每一次都查看当前节点的子节点是否还有对应需要检测的Mask,如果有就继续往下找,如果没有就停止,开始归的过程,这样能减少不必要的遍历。在归的过程中,调用callback函数完成对Flags的处理。

TypeScript 复制代码
/** 高阶函数 用来处理Effect */
function commitEffect(
  phrase: "mutation" | "layout",
  mask: Flags,
  callback: CommitCallback
): CommitCallback {
  /** 递归,DFS 找到最深的无subflags的节点 下面的不需要commit了 因为没有副作用 */
  return (finishedWork, root) => {
    // DFS
    let nextFinishedWork = finishedWork;
    while (nextFinishedWork !== null) {
      if (
        (nextFinishedWork.subTreeFlags & mask) !== NoFlags &&
        nextFinishedWork.child
      ) {
        // 递
        nextFinishedWork = nextFinishedWork.child;
      } else {
        while (nextFinishedWork !== null) {
          // 归
          callback(nextFinishedWork, root);
          if (nextFinishedWork.sibling !== null) {
            nextFinishedWork = nextFinishedWork.sibling;
            break;
          }
          nextFinishedWork = nextFinishedWork.return;
        }
      }
    }
  };
}
commitMutationEffectOnFIber

这个函数是传递进commitEffect的callback回调,commitEffect在每次归的过程中,都会调用这个函数处理每个节点。

这个函数的作用是,检查当前Fiber节点上的flag值,根据不同的flag,调用不同的Flag处理函数

TypeScript 复制代码
/** 用来处理 Mutation副作用 [Placement | Update | ChildDeletion // TODO PassiveEffect] */
const commitMutationEffectsOnFiber: CommitCallback = (finishedWork, root) => {
  // 处理每个节点的Effect
  // 获取节点的flags
  const flags = finishedWork.flags;
    // 处理placement
  if ((flags & Placement) !== NoFlags) {
    // 存在Placement
    commitPlacement(finishedWork);
    // 去掉副作用flag
    // 去掉某个flag: 0b0111&(~0b0100) => 0b0111&0b1011=> 0b0011 去掉了 0b0100
    finishedWork.flags &= ~Placement;
  }
    // 处理Update
  if ((flags & Update) !== NoFlags) {
    commitUpdate(finishedWork);
    finishedWork.flags &= ~Update;
  }

    // 处理ChildDeletion
  if ((flags & ChildDeletion) !== NoFlags) {
    const deletion = finishedWork.delections;
    deletion.forEach((deleteOldFiber) => {
      commitDeletion(deleteOldFiber, root);
    });
    finishedWork.flags &= ~ChildDeletion;
  }

    // 处理收集passiveEffect
  if ((flags & PassiveEffect) !== NoFlags) {
    // 存在被动副作用
    commitPassiveEffect(finishedWork, root, "update");
  }

  // 卸载Ref 只有hostComponent需要卸载
  if (finishedWork.tag === HostComponent && (flags & Ref) !== NoFlags) {
    const current = finishedWork.alternate;
    if (current) {
      // 需要卸载current的ref 其实本质上current和finishedWork的ref都是一个
      saftyDetachRef(current);
    }

    // 卸载之后由于可能还会加载ref 所以这里的flag不能~Ref
  }
};
commitPlacement 插入节点

commitPlacement函数用来把新创建的Fiber节点插入到当前DOM树上,插入节点需要找到要插入的父节点,以及要插入到父节点的哪个位置,即找到兄弟节点。

需要注意的点是:

  1. 只有HostComponent节点才能作为parent节点被插入

  2. 只有HostComponent节点或HostText节点才可以作为被插入节点的兄弟

由于函数节点 Fragment节点 Memo节点等仅在Fiber中体现,不存在于真实DOM中, 所以在寻找父节点和兄弟节点时需要注意。

TypeScript 复制代码
/** 处理Placement */
function commitPlacement(finishedWork: FiberNode) {
  /** 获取finishedWork的hostparent 用来挂载finishedWork对应的DOM (finishedWork可能也不是Host 后面有处理) */
  const hostParent = getHostParent(finishedWork) as Container;
  /** 获取finishedWork的Host sibling节点  */
  const hostSibling = getHostSibling(finishedWork) as Element;
  // 拿到parent和sibling了,就可以插入dom了
  // hostsibling不存在就是append 存在就是插入
  if (hostParent !== null) {
    insertOrAppendPlacementNodeIntoConatiner(
      finishedWork,
      hostParent,
      hostSibling
    );
  }
}
getHostparent

这个函数用来找到待插入节点最近的的父HostComponent节点,其实现如下,就是顺着return一直找,指导找到tag为HostComponent或者return为null(即HostRoot节点)

如果找到HostRoot节点,需要返回hostRoot.stateNode.container节点

TypeScript 复制代码
/** 获取HostParent
 *  获取当前节点的HostComponent/HostRoot parent
 */
function getHostParent(fiber: FiberNode): Element {
  let node = fiber.return;
  while (node !== null) {
    if (node.tag === HostComponent) {
      // host component 返回其stateNode
      return node.stateNode;
    }

    if (node.tag === HostRoot) {
      // hostRoot 其stateNode -> FiberRootNode 需要通过FiberRootNode.container获取
      return node.stateNode.container;
    }
    // 向上找
    node = node.return;
  }
  return null;
}
getHostSibling

这个函数用来找到和当前插入Fiber节点紧邻的兄弟节点,因为需要使用parent.apppendBefore(sibling)完成节点插入,所以需要找到其兄弟给插入位置定位。

其实现思路是,从当前节点先向上找一个存在sibling的父节点,任意类型都可以,如果在寻找的过程中,找到HostComponent HostRoot 或者 null 的时候,还没有sibling节点,说明当前节点没有兄弟节点,直接返回null (注意 HostComponent代表已经找到了其parent节点了,往上找没意义了)

如图:

实现如下,当while循环因为node.sibling不为null退出时,说明已经找到了存在sibling的父节点

TypeScript 复制代码
    while (node.sibling === null) {
      const parent = node.return;

      if (
        parent === null ||
        parent.tag === HostComponent ||
        parent.tag === HostRoot
      ) {
        /** 回溯的过程中 如果遇到了 hostComponent / hostRoot 说明找到了parent节点 不能再往上找了 */
        return null;
      }

      /** 继续往上查找 */
      node = parent;
    }

找到存在sibling的父节点后,就需要继续寻找sibling节点,从其sibling节点向下找child,如果child是HostComponent或者是HostText,则找到兄弟节点了,直接返回。

需要注意,这里找的节点一定要是非Placement的,因为Placement的节点还没有挂载在真实dom树上,无法作为兄弟节点,所以在向下找的过程中,不论是什么节点 是不是Host节点,只要其存在Placment Flag,都要结束这条路的查找,重新循环找父节兄弟的下一个兄弟,(如果一个节点为Placment,其下所有节点一定还没有挂载,可以不用再找了)如图

ul节点还没有被挂载,无法被使用

看一个能找到sibling的例子,如下:

其中Div为更新阶段挂载上去的,那么此时 div.sibling 已经存在,但是因为其不是Host元素,继续往下找到ul,符合要求,返回作为sibling

getHostSibling函数实现如下:

TypeScript 复制代码
/**
 * 查找fiber的sibling host节点 (难点)
 *  这里注意,sibling节点可能是不同级的
 *  同时 对于Placement的节点,由于其和其child节点都还没放置 不能作为sibling节点
 * 查找方向
 * 1. 查看当前节点有没有sibling,如果有从sibling往下找(child) 如果child为hostComponent/HostTag 并且flag不为placement 则返回
 *    如果查找的节点为placement 不论什么类型 查找和以下的节点都不能用 开始回溯
 * 2. 回溯,找查找节点的parent,如果有sibling 则回到 (1) 查找其sibling 直到找到一个不为placement的hostCom/hostText为止
 *    如果回溯的过程中,遇到了hostcomponent/hostroot 或者 null的节点 则直接返回null (因为回溯的过程中 一定走的都是非host节点 因为如果是host节点就肯定已经返回了)
 *    如果回溯到过程中遇到host 那么一定是parent节点 或者已经找到hostRoot了 表示没找到
 * @param fiber
 */
function getHostSibling(fiber: FiberNode): Element {
  let node = fiber;
  // 找sibling节点,没有找parent,如果遇到hostComponent / hostRoot 直接返回null
  findSibling: while (true) {
    while (node.sibling === null) {
      const parent = node.return;

      if (
        parent === null ||
        parent.tag === HostComponent ||
        parent.tag === HostRoot
      ) {
        /** 回溯的过程中 如果遇到了 hostComponent / hostRoot 说明找到了parent节点 不能再往上找了 */
        return null;
      }

      /** 继续往上查找 */
      node = parent;
    }

    // 执行到这里,说明存在sibling,移动node节点 -> sibling
    node.sibling.return = node.return;
    node = node.sibling;

    // 找到sibling了 此时开始向下查找,这里要注意,寻找的节点必须满足
    // 1. 是hostComponent / hostText
    // 2. 不能是placement节点 如果不满足,返回到回溯阶段
    while (node.tag !== HostComponent && node.tag !== HostText) {
      // 都不是,如果此时为Placement 下面的不用看了 因为当前节点下的DOM还没挂载,直接回溯
      if ((node.flags & Placement) !== NoFlags || node.child === null) {
        continue findSibling; // 直接跳到最外层循环,回溯
      }
      // 向下寻找
      node.child.return = node;
      node = node.child;
    }
    // 运行到此处 找到hostCompoent/hostText了 看是不是placement
    if ((node.flags & Placement) === NoFlags) {
      return node.stateNode;
    }
  }
}

找到parent和sibling节点之后,就要执行插入操作,其调用的函数是 insertOrAppendPlacementNodeIntoContainer 这个名字一看就知道什么意思,就是完成插入操作的.

insertOrAppendPlacementNodeIntoContainer

这个函数就是把当前的Placement节点插入DOM树,但是需要考虑多种情况

  1. 如果带插入节点是个Host节点,那么直接调用parent.appendBefore() 或者在没有sibling的情况下调用parent.appendChild() 即可

  2. 如果不是Host节点,可能是Fragment或者FunctionComponent这样的虚拟节点,那么就需要遍历其所有子节点,如果把如果是Host的节点都插入,当然了如果是非Host节点需要递归的寻找,也是一个深度优先的过程。 如图,插入节点为FunctionComponent时,其插入路径如下:

实现如下: 其中insert函数被递归调用,保证每个host节点都被挂载

TypeScript 复制代码
/**
 * 插入或者追加finishwork节点到hostParent(container)中
 * @param finishedWork
 * @param hostParent
 * @param hostSibling
 */
function insertOrAppendPlacementNodeIntoConatiner(
  finishedWork: FiberNode,
  hostParent: Container,
  hostSibling?: Element
) {
  // 这里需要注意 finishedWork 可能也不是HostComponet
  if (finishedWork.tag === HostComponent || finishedWork.tag == HostText) {
    if (hostSibling) {
      hostParent.insertBefore(finishedWork.stateNode, hostSibling);
    } else {
      hostParent.append(finishedWork.stateNode);
    }
  } else {
    // 如果finishwork不是host 比如是Fragment或者Function
    // 需要遍历其子节点 并且添加
    let child = finishedWork.child;
    while (child !== null) {
      insertOrAppendPlacementNodeIntoConatiner(child, hostParent, hostSibling);
      child = child.sibling;
    }
  }
}
commitUpdate更新节点

更新节点逻辑相对节点, 对于文本节点,修改其nodeValue

对于host节点,修改其dom节点上的 __props属性 (具体在合成事件中讲)

TypeScript 复制代码
/** 处理update副作用 */
function commitUpdate(fiber: FiberNode) {
  if (fiber.tag === HostText) {
    fiber.stateNode.nodeValue = fiber.memorizedProps.content;
  } else {
    updateFiberProps(fiber.stateNode, fiber.memorizedProps);
  }
}
commitDeletion 删除节点

删除节点其实和inserOrAppendPlacementNodeIntoConatiner 一样,先找到其parent的DOM节点,如果当前待删除的节点为hostComponent或HostText 直接删除,如果是其他节点,就需要递归的去寻找其第一层子节点,并且删除

TypeScript 复制代码
function commitDeletion(fiber: FiberNode, root: FiberRootNode) {
  const container = getHostParent(fiber);
  if (container) {
    deleteNodeFromContainer(container, fiber, root);
  }
}

/** 递归的方式删除节点 */
function deleteNodeFromContainer(
  container: Container,
  childToDelete: FiberNode,
  root: FiberRootNode
) {
  if (!container || !childToDelete) return;
  if ((childToDelete.tag === HostComponent || childToDelete.tag === HostText)&&childToDelete.stateNode!==null) {
    /** 如果是host节点,直接删除即可 */
    if(container.contains(childToDelete.stateNode)){
      container.removeChild(childToDelete.stateNode);
    }
  
    // 删除时,卸载Ref
    if (childToDelete.tag === HostComponent) {
      // HostComponent删除的时候 需要卸载Ref
      saftyDetachRef(childToDelete);
    }
  } else {
    /** 非host节点,递归删除 */
    if (childToDelete.tag === FunctionComponent) {
      /** 函数组件的情况下,需要收集Effect */
      commitPassiveEffect(childToDelete, root, "unmount");
    }
    let deleteNodeChild = childToDelete.child;
    while (deleteNodeChild !== null) {
      deleteNodeFromContainer(container, deleteNodeChild, root);
      deleteNodeChild = deleteNodeChild.sibling;
    }
  }
}

对于Ref和PassiveEffect 我们在后面说

执行完commitMutationEffects 会继续执行commitLayoutEffect, 其本质和MutationEffect一样,只不过mask换成了LayoutEffect = Ref 后面会细说Ref挂载更新卸载的流程

最后再次调度ensureRootIsScheduled

这样,我们就完成了简易的Commit流程!

相关推荐
学途路漫漫2 分钟前
怎么修改node_modules里的文件,怎么使用patch-package修改node_modules的文件,怎么修改第三方库原文件。
开发语言·javascript·ecmascript
蒜香拿铁27 分钟前
react-router的使用
前端·react.js
walking95744 分钟前
Vue 项目 PDF 预览插件指南
前端
2401_884810741 小时前
Vue3笔记
前端·vue.js·笔记
小画家~1 小时前
第二十四:5.2【搭建 pinia 环境】axios 异步调用数据
前端·vue.js
codexu1 小时前
Tauri跨端笔记实战(4) - 如何实现系统级截图
前端·前端框架·开源
过期生抽_1 小时前
如何快速上手Pinia!
前端
程序员黄同学1 小时前
请解释 React 中的 Hooks,何时使用 Hooks 更合适?
前端·javascript·react.js
朝阳391 小时前
Nuxt.js 3【详解】服务器 Server
服务器·javascript·nuxt.js
可可鸭~2 小时前
前端面试基础知识整理(一)
javascript·vue.js·学习·面试·elementui