[前端] React commit源码分析详解

2151 0
Honkers 2022-11-8 09:25:41 | 显示全部楼层 |阅读模式
目录

    总览commitBeforeMutationEffectscommitMutationEffects插入 dom 节点
      获取父节点及插入位置判断当前节点是否为单节点在对应位置插入节点
    更新 dom 节点更新 HostComponent更新 HostText删除 dom 节点unmountHostComponentscommitNestedUnmountscommitUnmountcommitLayoutEffects执行生命周期处理回调总结


总览

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 开始看:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function commitRoot(root) {
  3.   const renderPriorityLevel = getCurrentPriorityLevel();
  4.   runWithPriority(
  5.     ImmediateSchedulerPriority,
  6.     commitRootImpl.bind(null, root, renderPriorityLevel),
  7.   );
  8.   return null;
  9. }
复制代码
它调用了 commitRootImpl 函数,所要做的工作都在这个函数中:
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function commitRootImpl(root, renderPriorityLevel) {
  3.   // ...
  4.   const finishedWork = root.finishedWork;
  5.   const lanes = root.finishedLanes;
  6.   // ...
  7.   // 获取 effectList 链表
  8.   let firstEffect;
  9.   if (finishedWork.flags > PerformedWork) {
  10.     // 如果 root 上有 effect,则将其添加进 effectList 链表中
  11.     if (finishedWork.lastEffect !== null) {
  12.       finishedWork.lastEffect.nextEffect = finishedWork;
  13.       firstEffect = finishedWork.firstEffect;
  14.     } else {
  15.       firstEffect = finishedWork;
  16.     }
  17.   } else {
  18.     // 如果 root 上没有 effect,直接使用 finishedWork.firstEffect 作用链表头节点
  19.     firstEffect = finishedWork.firstEffect;
  20.   }
  21.   if (firstEffect !== null) {
  22.     // ...
  23.     // 第一次遍历,执行 commitBeforeMutationEffects
  24.     nextEffect = firstEffect;
  25.     do {
  26.       if (__DEV__) {
  27.         invokeGuardedCallback(null, commitBeforeMutationEffects, null);
  28.         // ...
  29.       } else {
  30.         try {
  31.           commitBeforeMutationEffects();
  32.         } catch (error) {
  33.           // ...
  34.         }
  35.       }
  36.     } while (nextEffect !== null);
  37.     // ...
  38.     // 第二次遍历,执行 commitMutationEffects
  39.     nextEffect = firstEffect;
  40.     do {
  41.       if (__DEV__) {
  42.         invokeGuardedCallback(
  43.           null,
  44.           commitMutationEffects,
  45.           null,
  46.           root,
  47.           renderPriorityLevel,
  48.         );
  49.         // ...
  50.       } else {
  51.         try {
  52.           commitMutationEffects(root, renderPriorityLevel);
  53.         } catch (error) {
  54.           // ...
  55.         }
  56.       }
  57.     } while (nextEffect !== null);
  58.     // 第三次遍历,执行 commitLayoutEffects
  59.     nextEffect = firstEffect;
  60.     do {
  61.       if (__DEV__) {
  62.         invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
  63.         // ...
  64.       } else {
  65.         try {
  66.           commitLayoutEffects(root, lanes);
  67.         } catch (error) {
  68.           // ...
  69.         }
  70.       }
  71.     } while (nextEffect !== null);
  72.     nextEffect = null;
  73.     // ...
  74.   } else {
  75.     // 没有任何副作用
  76.     root.current = finishedWork;
  77.     if (enableProfilerTimer) {
  78.       recordCommitTime();
  79.     }
  80.   }
  81.   // ...
  82. }
复制代码
commitBeforeMutationEffects

commitBeforeMutationEffects 中,会从 firstEffect 开始,通过 nextEffect 不断对 effectList 链表进行遍历,若是当前的 fiber 节点有 flags 副作用,则执行 commitBeforeMutationEffectOnFiber 节点去对针对 class 组件单独处理。
相关参考视频讲解:传送门
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function commitBeforeMutationEffects() {
  3.   while (nextEffect !== null) {
  4.     // ...
  5.     const flags = nextEffect.flags;
  6.     if ((flags & Snapshot) !== NoFlags) {
  7.       // 如果当前 fiber 节点有 flags 副作用
  8.       commitBeforeMutationEffectOnFiber(current, nextEffect);
  9.       // ...
  10.     }
  11.     // ...
  12.     nextEffect = nextEffect.nextEffect;
  13.   }
  14. }
复制代码
然后看一下 commitBeforeMutationEffectOnFiber,它里面根据 fiber 的 tag 属性,主要是对 ClassComponent 组件进行处理,更新 ClassComponent 实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitBeforeMutationLifeCycles(
  3.   current: Fiber | null,  finishedWork: Fiber,
  4. ): void {
  5.   switch (finishedWork.tag) {
  6.     case FunctionComponent:
  7.     case ForwardRef:
  8.     case SimpleMemoComponent:
  9.     case Block: {
  10.       return;
  11.     }
  12.     case ClassComponent: {
  13.       if (finishedWork.flags & Snapshot) {
  14.         if (current !== null) {
  15.           // 非首次加载的情况下
  16.           // 获取上一次的 props 和 state
  17.           const prevProps = current.memoizedProps;
  18.           const prevState = current.memoizedState;
  19.           // 获取当前 class 组件实例
  20.           const instance = finishedWork.stateNode;
  21.           // ...
  22.           // 调用 getSnapshotBeforeUpdate 生命周期方法
  23.           const snapshot = instance.getSnapshotBeforeUpdate(
  24.             finishedWork.elementType === finishedWork.type
  25.               ? prevProps
  26.               : resolveDefaultProps(finishedWork.type, prevProps),
  27.             prevState,
  28.           );
  29.           // ...
  30.           // 将生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上,供 DidUpdate 生命周期使用
  31.           instance.__reactInternalSnapshotBeforeUpdate = snapshot;
  32.         }
  33.       }
  34.       return;
  35.     }
  36.     // ...
  37.   }
  38. }
复制代码
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 执行更新操作
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function commitMutationEffects(
  3.   root: FiberRoot,  renderPriorityLevel: ReactPriorityLevel,
  4. ) {
  5.   // 对 effectList 进行遍历
  6.   while (nextEffect !== null) {
  7.     setCurrentDebugFiberInDEV(nextEffect);
  8.     const flags = nextEffect.flags;
  9.     // ContentReset:重置文本节点
  10.     if (flags & ContentReset) {
  11.       commitResetTextContent(nextEffect);
  12.     }
  13.     // Ref:commitDetachRef 更新 ref 的 current 值
  14.     if (flags & Ref) {
  15.       const current = nextEffect.alternate;
  16.       if (current !== null) {
  17.         commitDetachRef(current);
  18.       }
  19.       if (enableScopeAPI) {
  20.         if (nextEffect.tag === ScopeComponent) {
  21.           commitAttachRef(nextEffect);
  22.         }
  23.       }
  24.     }
  25.     // 执行更新、插入、删除操作
  26.     const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
  27.     switch (primaryFlags) {
  28.       case Placement: {
  29.         // 插入
  30.         commitPlacement(nextEffect);
  31.         nextEffect.flags &= ~Placement;
  32.         break;
  33.       }
  34.       case PlacementAndUpdate: {
  35.         // 插入并更新
  36.         // 插入
  37.         commitPlacement(nextEffect);
  38.         nextEffect.flags &= ~Placement;
  39.         // 更新
  40.         const current = nextEffect.alternate;
  41.         commitWork(current, nextEffect);
  42.         break;
  43.       }
  44.       // ...
  45.       case Update: {
  46.         // 更新
  47.         const current = nextEffect.alternate;
  48.         commitWork(current, nextEffect);
  49.         break;
  50.       }
  51.       case Deletion: {
  52.         // 删除
  53.         commitDeletion(root, nextEffect, renderPriorityLevel);
  54.         break;
  55.       }
  56.     }
  57.     resetCurrentDebugFiberInDEV();
  58.     nextEffect = nextEffect.nextEffect;
  59.   }
  60. }
复制代码
下面我们重点来看一下 react 是如何对真实 dom 节点进行操作的。

插入 dom 节点


获取父节点及插入位置

插入 dom 节点的操作以 commitPlacement 为入口函数, commitPlacement 中会首先获取当前 fiber 的父 fiber 对应的真实 dom 节点以及在父节点下要插入的位置,根据父节点对应的 dom 是否为 container,去执行 insertOrAppendPlacementNodeIntoContainer 或者 insertOrAppendPlacementNode 进行节点的插入。
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitPlacement(finishedWork: Fiber): void {
  3.   if (!supportsMutation) {
  4.     return;
  5.   }
  6.   // 获取当前 fiber 的父 fiber
  7.   const parentFiber = getHostParentFiber(finishedWork);
  8.   let parent;
  9.   let isContainer;
  10.   // 获取父 fiber 对应真实 dom 节点
  11.   const parentStateNode = parentFiber.stateNode;
  12.   // 获取父 fiber 对应的 dom 是否可以作为 container
  13.     case HostComponent:
  14.       parent = parentStateNode;
  15.       isContainer = false;
  16.       break;
  17.     case HostRoot:
  18.       parent = parentStateNode.containerInfo;
  19.       isContainer = true;
  20.       break;
  21.     case HostPortal:
  22.       parent = parentStateNode.containerInfo;
  23.       isContainer = true;
  24.       break;
  25.     case FundamentalComponent:
  26.       if (enableFundamentalAPI) {
  27.         parent = parentStateNode.instance;
  28.         isContainer = false;
  29.       }
  30.     default:
  31.       invariant(
  32.         false,
  33.         'Invalid host parent fiber. This error is likely caused by a bug ' +
  34.           'in React. Please file an issue.',
  35.       );
  36.   }
  37.   // 如果父 fiber 有 ContentReset 的 flags 副作用,则重置其文本内容
  38.   if (parentFiber.flags & ContentReset) {
  39.     resetTextContent(parent);
  40.     parentFiber.flags &= ~ContentReset;
  41.   }
  42.   // 获取要在哪个兄弟 fiber 之前插入
  43.   const before = getHostSibling(finishedWork);
  44.   if (isContainer) {
  45.     insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  46.   } else {
  47.     insertOrAppendPlacementNode(finishedWork, before, parent);
  48.   }
  49. }
复制代码
判断当前节点是否为单节点

我们以 insertOrAppendPlacementNodeIntoContainer 为例看一下其源码,里面通过 tag 属性判断了当前的 fiber 是否为原生 dom 节点。若是,则调用 insertInContainerBefore 或 appendChildToContainer 在相应位置插入真实 dom;若不是,则对当前 fiber 的所有子 fiber 调用 insertOrAppendPlacementNodeIntoContainer 进行遍历:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function insertOrAppendPlacementNodeIntoContainer(
  3.   node: Fiber,  before: ?Instance,  parent: Container,
  4. ): void {
  5.   const {tag} = node;
  6.   // 判断当前节点是否为原生的 dom 节点
  7.   const isHost = tag === HostComponent || tag === HostText;
  8.   if (isHost || (enableFundamentalAPI && tag === FundamentalComponent)) {
  9.     // 是原生 dom 节点,在父节点的对应位置插入当前节点
  10.     const stateNode = isHost ? node.stateNode : node.stateNode.instance;
  11.     if (before) {
  12.       insertInContainerBefore(parent, stateNode, before);
  13.     } else {
  14.       appendChildToContainer(parent, stateNode);
  15.     }
  16.   } else if (tag === HostPortal) {
  17.     // 如是 Portal 不做处理
  18.   } else {
  19.     // 不是原生 dom 节点,则遍历插入当前节点的各个子节点
  20.     const child = node.child;
  21.     if (child !== null) {
  22.       insertOrAppendPlacementNodeIntoContainer(child, before, parent);
  23.       let sibling = child.sibling;
  24.       while (sibling !== null) {
  25.         insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
  26.         sibling = sibling.sibling;
  27.       }
  28.     }
  29.   }
  30. }
复制代码
在对应位置插入节点

before 不为 null 时,说明要在某个 dom 节点之前插入新的 dom,调用 insertInContainerBefore 去进行插入,根据父节点是否注释类型,选择在父节点的父节点下插入新的 dom,还是直接在父节点下插入新的 dom:
  1. // packages/react-dom/src/client/ReactDOMHostConfig.js
  2. export function insertInContainerBefore(
  3.   container: Container,  child: Instance | TextInstance,  beforeChild: Instance | TextInstance | SuspenseInstance,
  4. ): void {
  5.   if (container.nodeType === COMMENT_NODE) {
  6.     // 如果父节点为注释类型,则在父节点的父节点下插入新的 dom
  7.     (container.parentNode: any).insertBefore(child, beforeChild);
  8.   } else {
  9.     // 否则直接插入新的 dom
  10.     container.insertBefore(child, beforeChild);
  11.   }
  12. }
复制代码
before 为 null 时,调用 appendChildToContainer 方法,直接在父节点(如果父节点为注释类型则在父节点的父节点)的最后位置插入新的 dom:
  1. export function appendChildToContainer(
  2.   container: Container,  child: Instance | TextInstance,
  3. ): void {
  4.   let parentNode;
  5.   if (container.nodeType === COMMENT_NODE) {
  6.     // 如果父节点为注释类型,则在父节点的父节点下插入新的 dom
  7.     parentNode = (container.parentNode: any);
  8.     parentNode.insertBefore(child, container);
  9.   } else {
  10.     // 否则直接插入新的 dom
  11.     parentNode = container;
  12.     parentNode.appendChild(child);
  13.   }
  14.   // ...
  15. }
复制代码
这几步都是以 insertOrAppendPlacementNodeIntoContainer 为例看源码,insertOrAppendPlacementNode 和它的唯一区别就是最后在对应位置插入节点时,不需要额外判断父节点 (container) 是否为 COMMENT_TYPE 了。

更新 dom 节点

更新操作以 commitWork 为入口函数,更新主要是针对 HostComponent 和 HostText 两种类型进行更新。
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  3.   // ...
  4.   switch (finishedWork.tag) {
  5.     // ...
  6.     case ClassComponent: {
  7.       return;
  8.     }
  9.     case HostComponent: {
  10.       // 获取真实 dom 节点
  11.       const instance: Instance = finishedWork.stateNode;
  12.       if (instance != null) {
  13.         // 获取新的 props
  14.         const newProps = finishedWork.memoizedProps;
  15.         // 获取老的 props
  16.         const oldProps = current !== null ? current.memoizedProps : newProps;
  17.         const type = finishedWork.type;
  18.         // 取出 updateQueue
  19.         const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
  20.         // 清空 updateQueue
  21.         finishedWork.updateQueue = null;
  22.         if (updatePayload !== null) {
  23.           // 提交更新
  24.           commitUpdate(
  25.             instance,
  26.             updatePayload,
  27.             type,
  28.             oldProps,
  29.             newProps,
  30.             finishedWork,
  31.           );
  32.         }
  33.       }
  34.       return;
  35.     }
  36.     case HostText: {
  37.       // 获取真实文本节点
  38.       const textInstance: TextInstance = finishedWork.stateNode;
  39.       // 获取新的文本内容
  40.       const newText: string = finishedWork.memoizedProps;
  41.       // 获取老的文本内容
  42.       const oldText: string =
  43.         current !== null ? current.memoizedProps : newText;
  44.       // 提交更新
  45.       commitTextUpdate(textInstance, oldText, newText);
  46.       return;
  47.     }
  48.     case HostRoot: {
  49.       // ssr操作,暂不关注
  50.       if (supportsHydration) {
  51.         const root: FiberRoot = finishedWork.stateNode;
  52.         if (root.hydrate) {
  53.           root.hydrate = false;
  54.           commitHydratedContainer(root.containerInfo);
  55.         }
  56.       }
  57.       return;
  58.     }
  59.     case Profiler: {
  60.       return;
  61.     }
  62.     // ...
  63. }
复制代码
更新 HostComponent

根据上面的 commitWork 的源码,更新 HostComponent 时,获取了真实 dom 节点实例、props 以及 updateQueue 之后,就调用 commitUpdate 对 dom 进行更新,它通过 updateProperties 函数将 props 变化应用到真实 dom 上。
  1. // packages/react-dom/src/client/ReactDOMHostConfig.js
  2. export function commitUpdate(
  3.   domElement: Instance,  updatePayload: Array<mixed>,  type: string,  oldProps: Props,  newProps: Props,  internalInstanceHandle: Object,
  4. ): void {
  5.   // 做了 domElement[internalPropsKey] = props 的操作
  6.   updateFiberProps(domElement, newProps);
  7.   // 应用给真实 dom
  8.   updateProperties(domElement, updatePayload, type, oldProps, newProps);
  9. }
复制代码
updateProperties 中,通过 updateDOMProperties 将 diff 结果应用于真实的 dom 节点。另外根据 fiber 的 tag 属性,如果判断对应的 dom 的节点为表单类型,例如 radio、textarea、input、select 等,会做特定的处理:
  1. // packages/react-dom/src/client/ReactDOMComponent.js
  2. export function updateProperties(
  3.   domElement: Element,  updatePayload: Array<any>,  tag: string,  lastRawProps: Object,  nextRawProps: Object,
  4. ): void {
  5.   // 针对表单组件进行特殊处理,例如更新 radio 的 checked 值
  6.   if (
  7.     tag === 'input' &&
  8.     nextRawProps.type === 'radio' &&
  9.     nextRawProps.name != null
  10.   ) {
  11.     ReactDOMInputUpdateChecked(domElement, nextRawProps);
  12.   }
  13.   // 判断是否为用户自定义的组件,即是否包含 "-"
  14.   const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
  15.   const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
  16.   // 将 diff 结果应用于真实 dom
  17.   updateDOMProperties(
  18.     domElement,
  19.     updatePayload,
  20.     wasCustomComponentTag,
  21.     isCustomComponentTag,
  22.   );
  23.   // 针对表单的特殊处理
  24.   switch (tag) {
  25.     case 'input':
  26.       ReactDOMInputUpdateWrapper(domElement, nextRawProps);
  27.       break;
  28.     case 'textarea':
  29.       ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
  30.       break;
  31.     case 'select':
  32.       ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
  33.       break;
  34.   }
  35. }
复制代码
updateDOMProperties 中,会遍历之前 render 阶段生成的 updatePayload,将其映射到真实的 dom 节点属性上,另外会针对 style、dangerouslySetInnerHTML 以及 textContent 做一些处理,从而实现了 dom 的更新:
  1. // packages/react-dom/src/client/ReactDOMHostConfig.js
  2. function updateDOMProperties(
  3.   domElement: Element,  updatePayload: Array<any>,  wasCustomComponentTag: boolean,  isCustomComponentTag: boolean,
  4. ): void {
  5.   // 对 updatePayload 遍历
  6.   for (let i = 0; i < updatePayload.length; i += 2) {
  7.     const propKey = updatePayload[i];
  8.     const propValue = updatePayload[i + 1];
  9.     if (propKey === STYLE) {
  10.       // 处理 style 样式更新
  11.       setValueForStyles(domElement, propValue);
  12.     } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
  13.       // 处理 innerHTML 改变
  14.       setInnerHTML(domElement, propValue);
  15.     } else if (propKey === CHILDREN) {
  16.       // 处理 textContent
  17.       setTextContent(domElement, propValue);
  18.     } else {
  19.       // 处理其他节点属性
  20.       setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
  21.     }
  22.   }
  23. }
复制代码
更新 HostText

HostText 的更新处理十分简单,调用 commitTextUpdate,里面直接将 dom 的 nodeValue 设置为 newText 的值:
  1. // packages/react-dom/src/client/ReactDOMHostConfig.js
  2. export function commitTextUpdate(
  3.   textInstance: TextInstance,  oldText: string,  newText: string,
  4. ): void {
  5.   textInstance.nodeValue = newText;
  6. }
复制代码
删除 dom 节点

删除 dom 节点的操作以 commitDeletion 为入口函数,它所要做的事情最复杂。react 会采用深度优先遍历去遍历整颗 fiber 树,找到需要删除的 fiber,除了要将对应的 dom 节点删除,还需要考虑 ref 的卸载、componentWillUnmount 等生命周期的调用:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitDeletion(
  3.   finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
  4. ): void {
  5.   if (supportsMutation) {
  6.     // 支持 useMutation
  7.     unmountHostComponents(finishedRoot, current, renderPriorityLevel);
  8.   } else {
  9.     // 不支持 useMutation
  10.     commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
  11.   }
  12.   const alternate = current.alternate;
  13.   // 重置 fiber 的各项属性
  14.   detachFiberMutation(current);
  15.   if (alternate !== null) {
  16.     detachFiberMutation(alternate);
  17.   }
  18. }
复制代码
unmountHostComponents

unmountHostComponents 首先判断当前父节点是否合法,若是不合法寻找合法的父节点,然后通过深度优先遍历,去遍历整棵树,通过 commitUnmount 卸载 ref、执行生命周期。遇到是原生 dom 类型的节点,还会从对应的父节点下删除该节点。
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function unmountHostComponents(
  3.   finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
  4. ): void {
  5.   let node: Fiber = current;
  6.   let currentParentIsValid = false;
  7.   let currentParent;
  8.   let currentParentIsContainer;
  9.   while (true) {
  10.     if (!currentParentIsValid) {
  11.       // 若当前的父节点不是非法的 dom 节点,寻找一个合法的 dom 父节点
  12.       let parent = node.return;
  13.       findParent: while (true) {
  14.         invariant(
  15.           parent !== null,
  16.           'Expected to find a host parent. This error is likely caused by ' +
  17.             'a bug in React. Please file an issue.',
  18.         );
  19.         const parentStateNode = parent.stateNode;
  20.         switch (parent.tag) {
  21.           case HostComponent:
  22.             currentParent = parentStateNode;
  23.             currentParentIsContainer = false;
  24.             break findParent;
  25.           case HostRoot:
  26.             currentParent = parentStateNode.containerInfo;
  27.             currentParentIsContainer = true;
  28.             break findParent;
  29.           case HostPortal:
  30.             currentParent = parentStateNode.containerInfo;
  31.             currentParentIsContainer = true;
  32.             break findParent;
  33.           case FundamentalComponent:
  34.             if (enableFundamentalAPI) {
  35.               currentParent = parentStateNode.instance;
  36.               currentParentIsContainer = false;
  37.             }
  38.         }
  39.         parent = parent.return;
  40.       }
  41.       currentParentIsValid = true;
  42.     }
  43.     if (node.tag === HostComponent || node.tag === HostText) {
  44.       // 若果是原生 dom 节点,调用 commitNestedUnmounts 方法
  45.       commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
  46.       if (currentParentIsContainer) {
  47.         // 若当前的 parent 是 container,则将 child 从 container 中移除(通过 dom.removeChild 方法)
  48.         removeChildFromContainer(
  49.           ((currentParent: any): Container),
  50.           (node.stateNode: Instance | TextInstance),
  51.         );
  52.       } else {
  53.         // 从 parent 中移除 child(通过 dom.removeChild 方法)
  54.         removeChild(
  55.           ((currentParent: any): Instance),
  56.           (node.stateNode: Instance | TextInstance),
  57.         );
  58.       }
  59.     } // ...
  60.     else if (node.tag === HostPortal) {
  61.       // 若是 portal 节点,直接向下遍历 child,因为它没有 ref 和生命周期等额外要处理的事情
  62.       if (node.child !== null) {
  63.         currentParent = node.stateNode.containerInfo;
  64.         currentParentIsContainer = true;
  65.         node.child.return = node;
  66.         node = node.child;
  67.         continue;
  68.       }
  69.     } else {
  70.       // 其他 react 节点,调用 commitUnmount,里面会卸载 ref、执行生命周期等
  71.       commitUnmount(finishedRoot, node, renderPriorityLevel);
  72.       // 深度优先遍历子节点
  73.       if (node.child !== null) {
  74.         node.child.return = node;
  75.         node = node.child;
  76.         continue;
  77.       }
  78.     }
  79.     // node 和 current 相等时说明整颗树的深度优先遍历完成
  80.     if (node === current) {
  81.       return;
  82.     }
  83.     // 如果没有兄弟节点,说明当前子树遍历完毕,返回到父节点继续深度优先遍历
  84.     while (node.sibling === null) {
  85.       if (node.return === null || node.return === current) {
  86.         return;
  87.       }
  88.       node = node.return;
  89.       if (node.tag === HostPortal) {
  90.         currentParentIsValid = false;
  91.       }
  92.     }
  93.     // 继续遍历兄弟节点
  94.     node.sibling.return = node.return;
  95.     node = node.sibling;
  96.   }
  97. }
复制代码
commitNestedUnmounts

commitNestedUnmounts 相比 unmountHostComponents 不需要额外做当前父节点是否合法的判断以及 react 节点类型的判断,直接采用深度优先遍历,去执行 commitUnmount 方法即可:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitNestedUnmounts(
  3.   finishedRoot: FiberRoot,  root: Fiber,  renderPriorityLevel: ReactPriorityLevel,
  4. ): void {
  5.   let node: Fiber = root;
  6.   while (true) {
  7.     // 调用 commitUnmount 去卸载 ref、执行生命周期
  8.     commitUnmount(finishedRoot, node, renderPriorityLevel);
  9.     if (
  10.       node.child !== null &&
  11.       (!supportsMutation || node.tag !== HostPortal)
  12.     ) {
  13.       // 深度优先遍历向下遍历子树
  14.       node.child.return = node;
  15.       node = node.child;
  16.       continue;
  17.     }
  18.     if (node === root) {
  19.       // node 为 root 时说明整棵树的深度优先遍历完成
  20.       return;
  21.     }
  22.     while (node.sibling === null) {
  23.       // node.sibling 为 null 时说明当前子树遍历完成,返回上级节点继续深度优先遍历
  24.       if (node.return === null || node.return === root) {
  25.         return;
  26.       }
  27.       node = node.return;
  28.     }
  29.     // 遍历兄弟节点
  30.     node.sibling.return = node.return;
  31.     node = node.sibling;
  32.   }
  33. }
复制代码
commitUnmount

commitUnmount 中会完成对 react 组件 ref 的卸载,若果是类组件,执行 componentWillUnmount 生命周期等操作:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitUnmount(
  3.   finishedRoot: FiberRoot,  current: Fiber,  renderPriorityLevel: ReactPriorityLevel,
  4. ): void {
  5.   onCommitUnmount(current);
  6.   switch (current.tag) {
  7.     case FunctionComponent:
  8.     case ForwardRef:
  9.     case MemoComponent:
  10.     case SimpleMemoComponent:
  11.     // ...
  12.     case ClassComponent: {
  13.       // 卸载 ref
  14.       safelyDetachRef(current);
  15.       const instance = current.stateNode;
  16.       // 执行 componentWillUnmount 生命周期
  17.       if (typeof instance.componentWillUnmount === 'function') {
  18.         safelyCallComponentWillUnmount(current, instance);
  19.       }
  20.       return;
  21.     }
  22.     case HostComponent: {
  23.       // 卸载 ref
  24.       safelyDetachRef(current);
  25.       return;
  26.     }
  27.     case HostPortal: {
  28.       if (supportsMutation) {
  29.         // 递归遍历子树
  30.         unmountHostComponents(finishedRoot, current, renderPriorityLevel);
  31.       } else if (supportsPersistence) {
  32.         emptyPortalContainer(current);
  33.       }
  34.       return;
  35.     }
  36.     // ...
  37.   }
  38. }
复制代码
最终通过以上操作,react 就完成了 dom 的删除工作。

commitLayoutEffects

接下来通过 commitLayoutEffects 为入口函数,执行第三次遍历,这里会遍历 effectList,执行 componentDidMount、componentDidUpdate 等生命周期,另外会执行 componentUpdateQueue 函数去执行回调函数。
  1. // packages/react-reconciler/src/ReactFiberWorkLoop.old.js
  2. function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
  3.   // ...
  4.   // 遍历 effectList
  5.   while (nextEffect !== null) {
  6.     setCurrentDebugFiberInDEV(nextEffect);
  7.     const flags = nextEffect.flags;
  8.     if (flags & (Update | Callback)) {
  9.       const current = nextEffect.alternate;
  10.       // 执行 componentDidMount、componentDidUpdate 以及 componentUpdateQueue
  11.       commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
  12.     }
  13.     // 更新 ref
  14.     if (enableScopeAPI) {
  15.       if (flags & Ref && nextEffect.tag !== ScopeComponent) {
  16.         commitAttachRef(nextEffect);
  17.       }
  18.     } else {
  19.       if (flags & Ref) {
  20.         commitAttachRef(nextEffect);
  21.       }
  22.     }
  23.     resetCurrentDebugFiberInDEV();
  24.     nextEffect = nextEffect.nextEffect;
  25.   }
  26. }
复制代码
执行生命周期

commitLayoutEffectOnFiber 调用了 packages/react-reconciler/src/ReactFiberCommitWork.old.js 路径下的 commitLifeCycles 函数,里面针对首次渲染和非首次渲染分别执行 componentDidMount 和 componentDidUpdate 生命周期,以及调用 commitUpdateQueue 去触发回调:
  1. // packages/react-reconciler/src/ReactFiberCommitWork.old.js
  2. function commitLifeCycles(
  3.   finishedRoot: FiberRoot,  current: Fiber | null,  finishedWork: Fiber,  committedLanes: Lanes,
  4. ): void {
  5.   switch (finishedWork.tag) {
  6.     case FunctionComponent:
  7.     case ForwardRef:
  8.     case SimpleMemoComponent:
  9.     // ...
  10.     case ClassComponent: {
  11.       const instance = finishedWork.stateNode;
  12.       if (finishedWork.flags & Update) {
  13.         if (current === null) {
  14.           // 首次渲染,执行 componentDidMount 生命周期
  15.           if (
  16.             enableProfilerTimer &&
  17.             enableProfilerCommitHooks &&
  18.             finishedWork.mode & ProfileMode
  19.           ) {
  20.             try {
  21.               startLayoutEffectTimer();
  22.               instance.componentDidMount();
  23.             } finally {
  24.               recordLayoutEffectDuration(finishedWork);
  25.             }
  26.           } else {
  27.             instance.componentDidMount();
  28.           }
  29.         } else {
  30.           // 非首次渲染,执行 componentDidUpdate 生命周期
  31.           const prevProps =
  32.             finishedWork.elementType === finishedWork.type
  33.               ? current.memoizedProps
  34.               : resolveDefaultProps(finishedWork.type, current.memoizedProps);
  35.           const prevState = current.memoizedState;
  36.           // ...
  37.           if (
  38.             enableProfilerTimer &&
  39.             enableProfilerCommitHooks &&
  40.             finishedWork.mode & ProfileMode
  41.           ) {
  42.             try {
  43.               startLayoutEffectTimer();
  44.               instance.componentDidUpdate(
  45.                 prevProps,
  46.                 prevState,
  47.                 instance.__reactInternalSnapshotBeforeUpdate,
  48.               );
  49.             } finally {
  50.               recordLayoutEffectDuration(finishedWork);
  51.             }
  52.           } else {
  53.             instance.componentDidUpdate(
  54.               prevProps,
  55.               prevState,
  56.               instance.__reactInternalSnapshotBeforeUpdate,
  57.             );
  58.           }
  59.         }
  60.       }
  61.       // ...
  62.       if (updateQueue !== null) {
  63.         // ...
  64.         // 执行 commitUpdateQueue 处理回调
  65.         commitUpdateQueue(finishedWork, updateQueue, instance);
  66.       }
  67.       return;
  68.     }
  69.     case HostRoot: {
  70.       const updateQueue: UpdateQueue<
  71.         *,
  72.       > | null = (finishedWork.updateQueue: any);
  73.       if (updateQueue !== null) {
  74.         // ...
  75.         // 调用 commitUpdateQueue 处理 ReactDOM.render 的回调
  76.         commitUpdateQueue(finishedWork, updateQueue, instance);
  77.       }
  78.       return;
  79.     }
  80.     case HostComponent: {
  81.       const instance: Instance = finishedWork.stateNode;
  82.       // ...
  83.       // commitMount 处理 input 标签有 auto-focus 的情况
  84.       if (current === null && finishedWork.flags & Update) {
  85.         const type = finishedWork.type;
  86.         const props = finishedWork.memoizedProps;
  87.         commitMount(instance, type, props, finishedWork);
  88.       }
  89.       return;
  90.     }
  91.     // ...
  92. }
复制代码
处理回调

处理回调是在 commitUpdateQueue 中做的,它会对 finishedQueue 上面的 effects 进行遍历,若有 callback,则执行 callback。同时会重置 finishedQueue 上面的 effects 为 null:
  1. // packages/react-reconciler/src/ReactUpdateQueue.old.js
  2. export function commitUpdateQueue<State>(
  3.   finishedWork: Fiber,
  4.   finishedQueue: UpdateQueue<State>,
  5.   instance: any,
  6. ): void {
  7.   const effects = finishedQueue.effects;
  8.   // 清空 effects
  9.   finishedQueue.effects = null;
  10.   // 对 effect 遍历
  11.   if (effects !== null) {
  12.     for (let i = 0; i < effects.length; i++) {
  13.       const effect = effects[i];
  14.       const callback = effect.callback;
  15.       // 执行回调
  16.       if (callback !== null) {
  17.         effect.callback = null;
  18.         callCallback(callback, instance);
  19.       }
  20.     }
  21.   }
  22. }
复制代码
在这之后就是进行最后一点变量还原等收尾工作,然后整个 commit 过程就完成了!

总结

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


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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Honkers

荣誉红客

关注
  • 4007
    主题
  • 36
    粉丝
  • 0
    关注
这家伙很懒,什么都没留下!

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2025 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行