React commit源码分析详解

目录
  • 总览
  • commitBeforeMutationEffects
  • commitMutationEffects
  • 插入 dom 节点
    • 获取父节点及插入位置
    • 判断当前节点是否为单节点
    • 在对应位置插入节点
  • 更新 dom 节点
  • 更新 HostComponent
  • 更新 HostText
  • 删除 dom 节点
  • unmountHostComponents
  • commitNestedUnmounts
  • commitUnmount
  • commitLayoutEffects
  • 执行生命周期
  • 处理回调
  • 总结

总览

commit 阶段相比于 render 阶段要简单很多,因为大部分更新的前期操作都在 render 阶段做好了,commit 阶段主要做的是根据之前生成的 effectList,对相应的真实 dom 进行更新和渲染,这个阶段是不可中断的。

commit 阶段大致可以分为以下几个过程:

  • 获取 effectList 链表,如果 root 上有 effect,则将其也添加进 effectList 中
  • 对 effectList 进行第一次遍历,执行 commitBeforeMutationEffects 函数来更新class组件实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数
  • 对 effectList 进行第二次遍历,执行 commitMutationEffects 函数来完成副作用的执行,主要包括重置文本节点以及真实 dom 节点的插入、删除和更新等操作。
  • 对 effectList 进行第三次遍历,执行 commitLayoutEffects 函数,去触发 componentDidMount、componentDidUpdate 以及各种回调函数等
  • 最后进行一点变量还原之类的收尾,就完成了 commit 阶段

我们从 commit 阶段的入口函数 commitRoot 开始看:

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitRoot(root) {
  const renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority(
    ImmediateSchedulerPriority,
    commitRootImpl.bind(null, root, renderPriorityLevel),
  );
  return null;
}

它调用了 commitRootImpl 函数,所要做的工作都在这个函数中:

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitRootImpl(root, renderPriorityLevel) {
  // ...
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  // ...
  // 获取 effectList 链表
  let firstEffect;
  if (finishedWork.flags > PerformedWork) {
    // 如果 root 上有 effect,则将其添加进 effectList 链表中
    if (finishedWork.lastEffect !== null) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // 如果 root 上没有 effect,直接使用 finishedWork.firstEffect 作用链表头节点
    firstEffect = finishedWork.firstEffect;
  }
  if (firstEffect !== null) {
    // ...
    // 第一次遍历,执行 commitBeforeMutationEffects
    nextEffect = firstEffect;
    do {
      if (__DEV__) {
        invokeGuardedCallback(null, commitBeforeMutationEffects, null);
        // ...
      } else {
        try {
          commitBeforeMutationEffects();
        } catch (error) {
          // ...
        }
      }
    } while (nextEffect !== null);
    // ...
    // 第二次遍历,执行 commitMutationEffects
    nextEffect = firstEffect;
    do {
      if (__DEV__) {
        invokeGuardedCallback(
          null,
          commitMutationEffects,
          null,
          root,
          renderPriorityLevel,
        );
        // ...
      } else {
        try {
          commitMutationEffects(root, renderPriorityLevel);
        } catch (error) {
          // ...
        }
      }
    } while (nextEffect !== null);
    // 第三次遍历,执行 commitLayoutEffects
    nextEffect = firstEffect;
    do {
      if (__DEV__) {
        invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
        // ...
      } else {
        try {
          commitLayoutEffects(root, lanes);
        } catch (error) {
          // ...
        }
      }
    } while (nextEffect !== null);
    nextEffect = null;
    // ...
  } else {
    // 没有任何副作用
    root.current = finishedWork;
    if (enableProfilerTimer) {
      recordCommitTime();
    }
  }
  // ...
}

commitBeforeMutationEffects

commitBeforeMutationEffects 中,会从 firstEffect 开始,通过 nextEffect 不断对 effectList 链表进行遍历,若是当前的 fiber 节点有 flags 副作用,则执行 commitBeforeMutationEffectOnFiber 节点去对针对 class 组件单独处理。

相关参考视频讲解:传送门

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    // ...
    const flags = nextEffect.flags;
    if ((flags & Snapshot) !== NoFlags) {
      // 如果当前 fiber 节点有 flags 副作用
      commitBeforeMutationEffectOnFiber(current, nextEffect);
      // ...
    }
    // ...
    nextEffect = nextEffect.nextEffect;
  }
}

然后看一下 commitBeforeMutationEffectOnFiber,它里面根据 fiber 的 tag 属性,主要是对 ClassComponent 组件进行处理,更新 ClassComponent 实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitBeforeMutationLifeCycles(
  current: Fiber | null,  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      return;
    }
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if (current !== null) {
          // 非首次加载的情况下
          // 获取上一次的 props 和 state
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          // 获取当前 class 组件实例
          const instance = finishedWork.stateNode;
          // ...
          // 调用 getSnapshotBeforeUpdate 生命周期方法
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // ...
          // 将生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上,供 DidUpdate 生命周期使用
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
    // ...
  }
}

commitMutationEffects

commitMutationEffects 中会根据对 effectList 进行第二次遍历,根据 flags 的类型进行二进制与操作,然后根据结果去执行不同的操作,对真实 dom 进行修改:相关参考视频讲解:进入学习

  • ContentReset: 如果 flags 中包含 ContentReset 类型,代表文本节点内容改变,则执行 commitResetTextContent 重置文本节点的内容
  • Ref: 如果 flags 中包含 Ref 类型,则执行 commitDetachRef 更改 ref 对应的 current 的值
  • Placement: 上一章 diff 中讲过 Placement 代表插入,会执行 commitPlacement 去插入 dom 节点
  • Update: flags 包含 Update 则会执行 commitWork 执行更新操作
  • Deletion: flags 包含 Deletion 则会执行 commitDeletion 执行更新操作
// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitMutationEffects(
  root: FiberRoot,  renderPriorityLevel: ReactPriorityLevel,
) {
  // 对 effectList 进行遍历
  while (nextEffect !== null) {
    setCurrentDebugFiberInDEV(nextEffect);
    const flags = nextEffect.flags;
    // ContentReset:重置文本节点
    if (flags & ContentReset) {
      commitResetTextContent(nextEffect);
    }
    // Ref:commitDetachRef 更新 ref 的 current 值
    if (flags & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
      if (enableScopeAPI) {
        if (nextEffect.tag === ScopeComponent) {
          commitAttachRef(nextEffect);
        }
      }
    }
    // 执行更新、插入、删除操作
    const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    switch (primaryFlags) {
      case Placement: {
        // 插入
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement;
        break;
      }
      case PlacementAndUpdate: {
        // 插入并更新
        // 插入
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement;
        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      // ...
      case Update: {
        // 更新
        const current = nextEffect.alternate;
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        // 删除
        commitDeletion(root, nextEffect, renderPriorityLevel);
        break;
      }
    }
    resetCurrentDebugFiberInDEV();
    nextEffect = nextEffect.nextEffect;
  }
}

下面我们重点来看一下 react 是如何对真实 dom 节点进行操作的。

插入 dom 节点

获取父节点及插入位置

插入 dom 节点的操作以 commitPlacement 为入口函数, commitPlacement 中会首先获取当前 fiber 的父 fiber 对应的真实 dom 节点以及在父节点下要插入的位置,根据父节点对应的 dom 是否为 container,去执行 insertOrAppendPlacementNodeIntoContainer 或者 insertOrAppendPlacementNode 进行节点的插入。

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }
  // 获取当前 fiber 的父 fiber
  const parentFiber = getHostParentFiber(finishedWork);
  let parent;
  let isContainer;
  // 获取父 fiber 对应真实 dom 节点
  const parentStateNode = parentFiber.stateNode;
  // 获取父 fiber 对应的 dom 是否可以作为 container
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case FundamentalComponent:
      if (enableFundamentalAPI) {
        parent = parentStateNode.instance;
        isContainer = false;
      }
    default:
      invariant(
        false,
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
  // 如果父 fiber 有 ContentReset 的 flags 副作用,则重置其文本内容
  if (parentFiber.flags & ContentReset) {
    resetTextContent(parent);
    parentFiber.flags &= ~ContentReset;
  }
  // 获取要在哪个兄弟 fiber 之前插入
  const before = getHostSibling(finishedWork);
  if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

判断当前节点是否为单节点

我们以 insertOrAppendPlacementNodeIntoContainer 为例看一下其源码,里面通过 tag 属性判断了当前的 fiber 是否为原生 dom 节点。若是,则调用 insertInContainerBeforeappendChildToContainer 在相应位置插入真实 dom;若不是,则对当前 fiber 的所有子 fiber 调用 insertOrAppendPlacementNodeIntoContainer 进行遍历:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,  before: ?Instance,  parent: Container,
): void {
  const {tag} = node;
  // 判断当前节点是否为原生的 dom 节点
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
    // 是原生 dom 节点,在父节点的对应位置插入当前节点
    const stateNode = isHost ? node.stateNode : node.stateNode.instance;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // 如是 Portal 不做处理
  } else {
    // 不是原生 dom 节点,则遍历插入当前节点的各个子节点
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

在对应位置插入节点

before 不为 null 时,说明要在某个 dom 节点之前插入新的 dom,调用 insertInContainerBefore 去进行插入,根据父节点是否注释类型,选择在父节点的父节点下插入新的 dom,还是直接在父节点下插入新的 dom:

// packages/react-dom/src/client/ReactDOMHostConfig.js
export function insertInContainerBefore(
  container: Container,  child: Instance | TextInstance,  beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
  if (container.nodeType === COMMENT_NODE) {
    // 如果父节点为注释类型,则在父节点的父节点下插入新的 dom
    (container.parentNode: any).insertBefore(child, beforeChild);
  } else {
    // 否则直接插入新的 dom
    container.insertBefore(child, beforeChild);
  }
}

before 为 null 时,调用 appendChildToContainer 方法,直接在父节点(如果父节点为注释类型则在父节点的父节点)的最后位置插入新的 dom:

export function appendChildToContainer(
  container: Container,  child: Instance | TextInstance,
): void {
  let parentNode;
  if (container.nodeType === COMMENT_NODE) {
    // 如果父节点为注释类型,则在父节点的父节点下插入新的 dom
    parentNode = (container.parentNode: any);
    parentNode.insertBefore(child, container);
  } else {
    // 否则直接插入新的 dom
    parentNode = container;
    parentNode.appendChild(child);
  }
  // ...
}

这几步都是以 insertOrAppendPlacementNodeIntoContainer 为例看源码,insertOrAppendPlacementNode 和它的唯一区别就是最后在对应位置插入节点时,不需要额外判断父节点 (container) 是否为 COMMENT_TYPE 了。

更新 dom 节点

更新操作以 commitWork 为入口函数,更新主要是针对 HostComponent 和 HostText 两种类型进行更新。

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  // ...
  switch (finishedWork.tag) {
    // ...
    case ClassComponent: {
      return;
    }
    case HostComponent: {
      // 获取真实 dom 节点
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // 获取新的 props
        const newProps = finishedWork.memoizedProps;
        // 获取老的 props
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // 取出 updateQueue
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        // 清空 updateQueue
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          // 提交更新
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }
    case HostText: {
      // 获取真实文本节点
      const textInstance: TextInstance = finishedWork.stateNode;
      // 获取新的文本内容
      const newText: string = finishedWork.memoizedProps;
      // 获取老的文本内容
      const oldText: string =
        current !== null ? current.memoizedProps : newText;
      // 提交更新
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }
    case HostRoot: {
      // ssr操作,暂不关注
      if (supportsHydration) {
        const root: FiberRoot = finishedWork.stateNode;
        if (root.hydrate) {
          root.hydrate = false;
          commitHydratedContainer(root.containerInfo);
        }
      }
      return;
    }
    case Profiler: {
      return;
    }
    // ...
}

更新 HostComponent

根据上面的 commitWork 的源码,更新 HostComponent 时,获取了真实 dom 节点实例、props 以及 updateQueue 之后,就调用 commitUpdate 对 dom 进行更新,它通过 updateProperties 函数将 props 变化应用到真实 dom 上。

// packages/react-dom/src/client/ReactDOMHostConfig.js
export function commitUpdate(
  domElement: Instance,  updatePayload: Array<mixed>,  type: string,  oldProps: Props,  newProps: Props,  internalInstanceHandle: Object,
): void {
  // 做了 domElement[internalPropsKey] = props 的操作
  updateFiberProps(domElement, newProps);
  // 应用给真实 dom
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

updateProperties 中,通过 updateDOMProperties 将 diff 结果应用于真实的 dom 节点。另外根据 fiber 的 tag 属性,如果判断对应的 dom 的节点为表单类型,例如 radio、textarea、input、select 等,会做特定的处理:

// packages/react-dom/src/client/ReactDOMComponent.js
export function updateProperties(
  domElement: Element,  updatePayload: Array<any>,  tag: string,  lastRawProps: Object,  nextRawProps: Object,
): void {
  // 针对表单组件进行特殊处理,例如更新 radio 的 checked 值
  if (
    tag === 'input' &&
    nextRawProps.type === 'radio' &&
    nextRawProps.name != null
  ) {
    ReactDOMInputUpdateChecked(domElement, nextRawProps);
  }
  // 判断是否为用户自定义的组件,即是否包含 "-"
  const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
  const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
  // 将 diff 结果应用于真实 dom
  updateDOMProperties(
    domElement,
    updatePayload,
    wasCustomComponentTag,
    isCustomComponentTag,
  );
  // 针对表单的特殊处理
  switch (tag) {
    case 'input':
      ReactDOMInputUpdateWrapper(domElement, nextRawProps);
      break;
    case 'textarea':
      ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
      break;
    case 'select':
      ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
      break;
  }
}

updateDOMProperties 中,会遍历之前 render 阶段生成的 updatePayload,将其映射到真实的 dom 节点属性上,另外会针对 style、dangerouslySetInnerHTML 以及 textContent 做一些处理,从而实现了 dom 的更新:

// packages/react-dom/src/client/ReactDOMHostConfig.js
function updateDOMProperties(
  domElement: Element,  updatePayload: Array<any>,  wasCustomComponentTag: boolean,  isCustomComponentTag: boolean,
): void {
  // 对 updatePayload 遍历
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    if (propKey === STYLE) {
      // 处理 style 样式更新
      setValueForStyles(domElement, propValue);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 处理 innerHTML 改变
      setInnerHTML(domElement, propValue);
    } else if (propKey === CHILDREN) {
      // 处理 textContent
      setTextContent(domElement, propValue);
    } else {
      // 处理其他节点属性
      setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
    }
  }
}

更新 HostText

HostText 的更新处理十分简单,调用 commitTextUpdate,里面直接将 dom 的 nodeValue 设置为 newText 的值:

// packages/react-dom/src/client/ReactDOMHostConfig.js
export function commitTextUpdate(
  textInstance: TextInstance,  oldText: string,  newText: string,
): void {
  textInstance.nodeValue = newText;
}

删除 dom 节点

删除 dom 节点的操作以 commitDeletion 为入口函数,它所要做的事情最复杂。react 会采用深度优先遍历去遍历整颗 fiber 树,找到需要删除的 fiber,除了要将对应的 dom 节点删除,还需要考虑 ref 的卸载、componentWillUnmount 等生命周期的调用:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitDeletion(
  finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
): void {
  if (supportsMutation) {
    // 支持 useMutation
    unmountHostComponents(finishedRoot, current, renderPriorityLevel);
  } else {
    // 不支持 useMutation
    commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
  }
  const alternate = current.alternate;
  // 重置 fiber 的各项属性
  detachFiberMutation(current);
  if (alternate !== null) {
    detachFiberMutation(alternate);
  }
}

unmountHostComponents

unmountHostComponents 首先判断当前父节点是否合法,若是不合法寻找合法的父节点,然后通过深度优先遍历,去遍历整棵树,通过 commitUnmount 卸载 ref、执行生命周期。遇到是原生 dom 类型的节点,还会从对应的父节点下删除该节点。

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function unmountHostComponents(
  finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
): void {
  let node: Fiber = current;
  let currentParentIsValid = false;
  let currentParent;
  let currentParentIsContainer;
  while (true) {
    if (!currentParentIsValid) {
      // 若当前的父节点不是非法的 dom 节点,寻找一个合法的 dom 父节点
      let parent = node.return;
      findParent: while (true) {
        invariant(
          parent !== null,
          'Expected to find a host parent. This error is likely caused by ' +
            'a bug in React. Please file an issue.',
        );
        const parentStateNode = parent.stateNode;
        switch (parent.tag) {
          case HostComponent:
            currentParent = parentStateNode;
            currentParentIsContainer = false;
            break findParent;
          case HostRoot:
            currentParent = parentStateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
          case HostPortal:
            currentParent = parentStateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
          case FundamentalComponent:
            if (enableFundamentalAPI) {
              currentParent = parentStateNode.instance;
              currentParentIsContainer = false;
            }
        }
        parent = parent.return;
      }
      currentParentIsValid = true;
    }
    if (node.tag === HostComponent || node.tag === HostText) {
      // 若果是原生 dom 节点,调用 commitNestedUnmounts 方法
      commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
      if (currentParentIsContainer) {
        // 若当前的 parent 是 container,则将 child 从 container 中移除(通过 dom.removeChild 方法)
        removeChildFromContainer(
          ((currentParent: any): Container),
          (node.stateNode: Instance | TextInstance),
        );
      } else {
        // 从 parent 中移除 child(通过 dom.removeChild 方法)
        removeChild(
          ((currentParent: any): Instance),
          (node.stateNode: Instance | TextInstance),
        );
      }
    } // ...
    else if (node.tag === HostPortal) {
      // 若是 portal 节点,直接向下遍历 child,因为它没有 ref 和生命周期等额外要处理的事情
      if (node.child !== null) {
        currentParent = node.stateNode.containerInfo;
        currentParentIsContainer = true;
        node.child.return = node;
        node = node.child;
        continue;
      }
    } else {
      // 其他 react 节点,调用 commitUnmount,里面会卸载 ref、执行生命周期等
      commitUnmount(finishedRoot, node, renderPriorityLevel);
      // 深度优先遍历子节点
      if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }
    }
    // node 和 current 相等时说明整颗树的深度优先遍历完成
    if (node === current) {
      return;
    }
    // 如果没有兄弟节点,说明当前子树遍历完毕,返回到父节点继续深度优先遍历
    while (node.sibling === null) {
      if (node.return === null || node.return === current) {
        return;
      }
      node = node.return;
      if (node.tag === HostPortal) {
        currentParentIsValid = false;
      }
    }
    // 继续遍历兄弟节点
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

commitNestedUnmounts

commitNestedUnmounts 相比 unmountHostComponents 不需要额外做当前父节点是否合法的判断以及 react 节点类型的判断,直接采用深度优先遍历,去执行 commitUnmount 方法即可:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitNestedUnmounts(
  finishedRoot: FiberRoot,  root: Fiber,  renderPriorityLevel: ReactPriorityLevel,
): void {
  let node: Fiber = root;
  while (true) {
    // 调用 commitUnmount 去卸载 ref、执行生命周期
    commitUnmount(finishedRoot, node, renderPriorityLevel);
    if (
      node.child !== null &&
      (!supportsMutation || node.tag !== HostPortal)
    ) {
      // 深度优先遍历向下遍历子树
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === root) {
      // node 为 root 时说明整棵树的深度优先遍历完成
      return;
    }
    while (node.sibling === null) {
      // node.sibling 为 null 时说明当前子树遍历完成,返回上级节点继续深度优先遍历
      if (node.return === null || node.return === root) {
        return;
      }
      node = node.return;
    }
    // 遍历兄弟节点
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

commitUnmount

commitUnmount 中会完成对 react 组件 ref 的卸载,若果是类组件,执行 componentWillUnmount 生命周期等操作:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitUnmount(
  finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
): void {
  onCommitUnmount(current);
  switch (current.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent:
    // ...
    case ClassComponent: {
      // 卸载 ref
      safelyDetachRef(current);
      const instance = current.stateNode;
      // 执行 componentWillUnmount 生命周期
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(current, instance);
      }
      return;
    }
    case HostComponent: {
      // 卸载 ref
      safelyDetachRef(current);
      return;
    }
    case HostPortal: {
      if (supportsMutation) {
        // 递归遍历子树
        unmountHostComponents(finishedRoot, current, renderPriorityLevel);
      } else if (supportsPersistence) {
        emptyPortalContainer(current);
      }
      return;
    }
    // ...
  }
}

最终通过以上操作,react 就完成了 dom 的删除工作。

commitLayoutEffects

接下来通过 commitLayoutEffects 为入口函数,执行第三次遍历,这里会遍历 effectList,执行 componentDidMountcomponentDidUpdate 等生命周期,另外会执行 componentUpdateQueue 函数去执行回调函数。

// packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  // ...
  // 遍历 effectList
  while (nextEffect !== null) {
    setCurrentDebugFiberInDEV(nextEffect);
    const flags = nextEffect.flags;
    if (flags & (Update | Callback)) {
      const current = nextEffect.alternate;
      // 执行 componentDidMount、componentDidUpdate 以及 componentUpdateQueue
      commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
    }
    // 更新 ref
    if (enableScopeAPI) {
      if (flags & Ref && nextEffect.tag !== ScopeComponent) {
        commitAttachRef(nextEffect);
      }
    } else {
      if (flags & Ref) {
        commitAttachRef(nextEffect);
      }
    }
    resetCurrentDebugFiberInDEV();
    nextEffect = nextEffect.nextEffect;
  }
}

执行生命周期

commitLayoutEffectOnFiber 调用了 packages/react-reconciler/src/ReactFiberCommitWork.old.js 路径下的 commitLifeCycles 函数,里面针对首次渲染和非首次渲染分别执行 componentDidMountcomponentDidUpdate 生命周期,以及调用 commitUpdateQueue 去触发回调:

// packages/react-reconciler/src/ReactFiberCommitWork.old.js
function commitLifeCycles(
  finishedRoot: FiberRoot,  current: Fiber | null,  finishedWork: Fiber,  committedLanes: Lanes,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    // ...
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (finishedWork.flags & Update) {
        if (current === null) {
          // 首次渲染,执行 componentDidMount 生命周期
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              instance.componentDidMount();
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            instance.componentDidMount();
          }
        } else {
          // 非首次渲染,执行 componentDidUpdate 生命周期
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;
          // ...
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              instance.componentDidUpdate(
                prevProps,
                prevState,
                instance.__reactInternalSnapshotBeforeUpdate,
              );
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            instance.componentDidUpdate(
              prevProps,
              prevState,
              instance.__reactInternalSnapshotBeforeUpdate,
            );
          }
        }
      }
      // ...
      if (updateQueue !== null) {
        // ...
        // 执行 commitUpdateQueue 处理回调
        commitUpdateQueue(finishedWork, updateQueue, instance);
      }
      return;
    }
    case HostRoot: {
      const updateQueue: UpdateQueue<
        *,
      > | null = (finishedWork.updateQueue: any);
      if (updateQueue !== null) {
        // ...
        // 调用 commitUpdateQueue 处理 ReactDOM.render 的回调
        commitUpdateQueue(finishedWork, updateQueue, instance);
      }
      return;
    }
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      // ...
      // commitMount 处理 input 标签有 auto-focus 的情况
      if (current === null && finishedWork.flags & Update) {
        const type = finishedWork.type;
        const props = finishedWork.memoizedProps;
        commitMount(instance, type, props, finishedWork);
      }
      return;
    }
    // ...
}

处理回调

处理回调是在 commitUpdateQueue 中做的,它会对 finishedQueue 上面的 effects 进行遍历,若有 callback,则执行 callback。同时会重置 finishedQueue 上面的 effects 为 null:

// packages/react-reconciler/src/ReactUpdateQueue.old.js
export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
): void {
  const effects = finishedQueue.effects;
  // 清空 effects
  finishedQueue.effects = null;
  // 对 effect 遍历
  if (effects !== null) {
    for (let i = 0; i < effects.length; i++) {
      const effect = effects[i];
      const callback = effect.callback;
      // 执行回调
      if (callback !== null) {
        effect.callback = null;
        callCallback(callback, instance);
      }
    }
  }
}

在这之后就是进行最后一点变量还原等收尾工作,然后整个 commit 过程就完成了!

总结

接 render 阶段的流程图,补充上 commit 阶段的流程图,就构成了完整的 react 执行图了:

到此这篇关于React commit源码分析详解的文章就介绍到这了,更多相关React commit内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React Fiber源码深入分析

    目录 前言 React架构前世今生 React@15及之前 React@16及之后 Fiber Fiber简单理解 Fiber结构 Fiber工作原理 mount update 前言 本次React源码参考版本为17.0.3. React架构前世今生 查阅文档了解到, React@16.x是个分水岭. React@15及之前 在16之前,React架构大致可以分为两层: Reconciler: 主要职责是对比查找更新前后的变化的组件: Renderer: 主要职责是基于变化渲染页面: 但是Rea

  • React深入分析useEffect源码

    目录 热身准备 初始化 mount 更新 update updateEffect 执行副作用 总结 热身准备 这里不再讲useLayoutEffect,它和useEffect的代码是一样的,区别主要是: 执行时机不同: useEffect是异步, useLayoutEffect是同步,会阻塞渲染: 初始化 mount mountEffect 在所有hook初始化时都会通过下面这行代码实现hook结构的初始化和存储,这里不再讲mountWorkInProgressHook方法 var hook =

  • React前端开发createElement源码解读

    目录 React 与 Babel 元素标签转译 组件转译 子元素转译 createElement 源码 函数入参 第一段代码 __self 和 __source 第二段代码 props 对象 第三段代码 children 第四段代码 defaultProps 第五段代码 owner ReactElement 源码 REACT_ELEMENT_TYPE 回顾 React 与 Babel 元素标签转译 用过 React 的同学都知道,当我们这样写时: <div id="foo">

  • React超详细分析useState与useReducer源码

    目录 热身准备 为什么会有hooks hooks执行时机 两套hooks hooks存储 初始化 mount useState mountWorkInProgressHook 更新update updateState updateReducer updateWorkInProgressHook 总结 热身准备 在正式讲useState,我们先热热身,了解下必备知识. 为什么会有hooks 大家都知道hooks是在函数组件的产物.之前class组件为什么没有出现hooks这种东西呢? 答案很简单,

  • React commit源码分析详解

    目录 总览 commitBeforeMutationEffects commitMutationEffects 插入 dom 节点 获取父节点及插入位置 判断当前节点是否为单节点 在对应位置插入节点 更新 dom 节点 更新 HostComponent 更新 HostText 删除 dom 节点 unmountHostComponents commitNestedUnmounts commitUnmount commitLayoutEffects 执行生命周期 处理回调 总结 总览 commit

  • Python日志打印里logging.getLogger源码分析详解

    实践环境 WIN 10 Python 3.6.5 函数说明 logging.getLogger(name=None) getLogger函数位于logging/__init__.py脚本 源码分析 _loggerClass = Logger # ...略 root = RootLogger(WARNING) Logger.root = root Logger.manager = Manager(Logger.root) # ...略 def getLogger(name=None): "&quo

  • SpringCloud微服务续约实现源码分析详解

    目录 一.前言 二.客户端续约 1.入口 构造初始化 initScheduledTasks()调度执行心跳任务 2.TimedSupervisorTask组件 构造初始化 TimedSupervisorTask#run()任务逻辑 3.心跳任务 HeartbeatThread私有内部类 发送心跳 4.发送心跳到注册中心 构建请求数据发送心跳 三.服务端处理客户端续约 1.InstanceRegistry#renew()逻辑 2.PeerAwareInstanceRegistryImpl#rene

  • python django事务transaction源码分析详解

    python Django事务 网上关于django1.6的事务资料很多,但是1.8的却搜不到任何资料,自己要用的时候费了不少劲就是不行,现在记下要用的人少走弯路 version:Django 1.8 事务官方文档 事务中文文档里面介绍很多方法,不一一赘述,按照文档即可,下面只分析下atomic方法的源码 按照官方文档 transaction.atomic 有两种用法装饰器和上下文管理器 # atomic() 方法 # from django.db import transaction ####

  • Java Spring @Lazy延迟注入源码案例详解

    前言 有时候我们会在属性注入的时候添加@Lazy注解实现延迟注入,今天咱们通过阅读源码来分析下原因 一.一个简单的小例子 代码如下: @Service public class NormalService1 { @Autowired @Lazy private MyService myService; public void doSomething() { myService.getName(); } } 作用是为了进行延迟加载,在NormalService1进行属性注入的时候,如果MyServ

  • Android Handler,Message,MessageQueue,Loper源码解析详解

    本文主要是对Handler和消息循环的实现原理进行源码分析,如果不熟悉Handler可以参见博文< Android中Handler的使用>,里面对Android为何以引入Handler机制以及如何使用Handler做了讲解. 概括来说,Handler是Android中引入的一种让开发者参与处理线程中消息循环的机制.我们在使用Handler的时候与Message打交道最多,Message是Hanlder机制向开发人员暴露出来的相关类,可以通过Message类完成大部分操作Handler的功能.但

  • Spring源码BeanFactoryPostProcessor详解

    Spring源码分析-BeanFactoryPostProcessor BeanFactoryPostProcessor接口是Spring提供的对Bean的扩展点,它的子接口是BeanDefinitionRegistryPostProcessor @FunctionalInterface public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory b

  • Node.js高级编程cluster环境及源码调试详解

    目录 前言 准备调试环境 编译 Node.js 准备 IDE 环境 Cluster 源码调试 SharedHandle RoundRobinHandle 为什么端口不冲突 SO_REUSEADDR 补充 SharedHandle 和 RoundRobinHandle 两种模式的对比 前言 日常工作中,对 Node.js 的使用都比较粗浅,趁未羊之际,来学点稍微高级的,那就先从 cluster 开始吧. 尼古拉斯张三说过,“带着问题去学习是一个比较好的方法”,所以我们也来试一试. 当初使用 clu

  • Golang HTTP编程的源码解析详解

    目录 1.网络基础 2.Golang HTTP编程 2.1 代码示例 2.2 源码分析 3. 总结 1.网络基础 基本TCP客户-服务器程序Socket编程流程如如下图所示. TCP服务器绑定到特定端口并阻塞监听客户端端连接, TCP客户端则通过IP+端口向服务器发起请求,客户-服务器建立连接之后就能开始进行数据传输. Golang的TCP编程也是基于上述流程的. 2.Golang HTTP编程 2.1 代码示例 func timeHandler(w http.ResponseWriter, r

  • java TreeMap源码解析详解

    java TreeMap源码解析详解 在介绍TreeMap之前,我们来了解一种数据结构:排序二叉树.相信学过数据结构的同学知道,这种结构的数据存储形式在查找的时候效率非常高. 如图所示,这种数据结构是以二叉树为基础的,所有的左孩子的value值都是小于根结点的value值的,所有右孩子的value值都是大于根结点的.这样做的好处在于:如果需要按照键值查找数据元素,只要比较当前结点的value值即可(小于当前结点value值的,往左走,否则往右走),这种方式,每次可以减少一半的操作,所以效率比较高

随机推荐