翼度科技»论坛 编程开发 JavaScript 查看内容

React commit源码分析详解

7

主题

7

帖子

21

积分

新手上路

Rank: 1

积分
21
总览

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

  • 获取 effectList 链表,如果 root 上有 effect,则将其也添加进 effectList 中
  • 对 effectList 进行第一次遍历,执行
    1. commitBeforeMutationEffects
    复制代码
    函数来更新class组件实例上的state、props 等,以及执行 getSnapshotBeforeUpdate 生命周期函数
  • 对 effectList 进行第二次遍历,执行
    1. commitMutationEffects
    复制代码
    函数来完成副作用的执行,主要包括重置文本节点以及真实 dom 节点的插入、删除和更新等操作。
  • 对 effectList 进行第三次遍历,执行
    1. commitLayoutEffects
    复制代码
    函数,去触发 componentDidMount、componentDidUpdate 以及各种回调函数等
  • 最后进行一点变量还原之类的收尾,就完成了 commit 阶段
我们从 commit 阶段的入口函数
  1. 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. }
复制代码
它调用了
  1. 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
  1. commitBeforeMutationEffects
复制代码
中,会从 firstEffect 开始,通过 nextEffect 不断对 effectList 链表进行遍历,若是当前的 fiber 节点有 flags 副作用,则执行
  1. 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. }
复制代码
然后看一下
  1. 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
  1. commitMutationEffects
复制代码
中会根据对 effectList 进行第二次遍历,根据 flags 的类型进行二进制与操作,然后根据结果去执行不同的操作,对真实 dom 进行修改:相关参考视频讲解:进入学习

  • ContentReset: 如果 flags 中包含 ContentReset 类型,代表文本节点内容改变,则执行
    1. commitResetTextContent
    复制代码
    重置文本节点的内容
  • Ref: 如果 flags 中包含 Ref 类型,则执行
    1. commitDetachRef
    复制代码
    更改 ref 对应的 current 的值
  • Placement: 上一章 diff 中讲过 Placement 代表插入,会执行
    1. commitPlacement
    复制代码
    去插入 dom 节点
  • Update: flags 包含 Update 则会执行
    1. commitWork
    复制代码
    执行更新操作
  • Deletion: flags 包含 Deletion 则会执行
    1. 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 节点的操作以
  1. commitPlacement
复制代码
为入口函数,
  1. commitPlacement
复制代码
中会首先获取当前 fiber 的父 fiber 对应的真实 dom 节点以及在父节点下要插入的位置,根据父节点对应的 dom 是否为 container,去执行
  1. insertOrAppendPlacementNodeIntoContainer
复制代码
或者
  1. 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. }
复制代码
判断当前节点是否为单节点

我们以
  1. insertOrAppendPlacementNodeIntoContainer
复制代码
为例看一下其源码,里面通过 tag 属性判断了当前的 fiber 是否为原生 dom 节点。若是,则调用
  1. insertInContainerBefore
复制代码
  1. appendChildToContainer
复制代码
在相应位置插入真实 dom;若不是,则对当前 fiber 的所有子 fiber 调用
  1. 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,调用
  1. 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 时,调用
  1. 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. }
复制代码
这几步都是以
  1. insertOrAppendPlacementNodeIntoContainer
复制代码
为例看源码,
  1. insertOrAppendPlacementNode
复制代码
和它的唯一区别就是最后在对应位置插入节点时,不需要额外判断父节点 (container) 是否为 COMMENT_TYPE 了。

更新 dom 节点

更新操作以
  1. 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 之后,就调用
  1. commitUpdate
复制代码
对 dom 进行更新,它通过
  1. 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. }
复制代码
  1. updateProperties
复制代码
中,通过
  1. 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. }
复制代码
  1. 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 的更新处理十分简单,调用
  1. 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 节点的操作以
  1. 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
  1. unmountHostComponents
复制代码
首先判断当前父节点是否合法,若是不合法寻找合法的父节点,然后通过深度优先遍历,去遍历整棵树,通过
  1. 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
  1. commitNestedUnmounts
复制代码
相比
  1. unmountHostComponents
复制代码
不需要额外做当前父节点是否合法的判断以及 react 节点类型的判断,直接采用深度优先遍历,去执行
  1. 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

接下来通过
  1. commitLayoutEffects
复制代码
为入口函数,执行第三次遍历,这里会遍历 effectList,执行
  1. componentDidMount
复制代码
  1. componentDidUpdate
复制代码
等生命周期,另外会执行
  1. 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. }
复制代码
执行生命周期
  1. commitLayoutEffectOnFiber
复制代码
调用了
  1. packages/react-reconciler/src/ReactFiberCommitWork.old.js
复制代码
路径下的
  1. commitLifeCycles
复制代码
函数,里面针对首次渲染和非首次渲染分别执行
  1. componentDidMount
复制代码
  1. componentDidUpdate
复制代码
生命周期,以及调用
  1. 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. }
复制代码
处理回调

处理回调是在
  1. 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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/article/266749.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

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

x

举报 回复 使用道具