终于到了执行DOM操作的mutation阶段

概览

类似before mutation阶段mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects

  1. nextEffect = firstEffect;
  2. do {
  3. try {
  4. commitMutationEffects(root, renderPriorityLevel);
  5. } catch (error) {
  6. invariant(nextEffect !== null, 'Should be working on an effect.');
  7. captureCommitPhaseError(nextEffect, error);
  8. nextEffect = nextEffect.nextEffect;
  9. }
  10. } while (nextEffect !== null);

commitMutationEffects

代码如下:

你可以在这里mutation阶段 - 图1 (opens new window)看到commitMutationEffects源码

  1. function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  2. // 遍历effectList
  3. while (nextEffect !== null) {
  4. const effectTag = nextEffect.effectTag;
  5. // 根据 ContentReset effectTag重置文字节点
  6. if (effectTag & ContentReset) {
  7. commitResetTextContent(nextEffect);
  8. }
  9. // 更新ref
  10. if (effectTag & Ref) {
  11. const current = nextEffect.alternate;
  12. if (current !== null) {
  13. commitDetachRef(current);
  14. }
  15. }
  16. // 根据 effectTag 分别处理
  17. const primaryEffectTag =
  18. effectTag & (Placement | Update | Deletion | Hydrating);
  19. switch (primaryEffectTag) {
  20. // 插入DOM
  21. case Placement: {
  22. commitPlacement(nextEffect);
  23. nextEffect.effectTag &= ~Placement;
  24. break;
  25. }
  26. // 插入DOM 并 更新DOM
  27. case PlacementAndUpdate: {
  28. // 插入
  29. commitPlacement(nextEffect);
  30. nextEffect.effectTag &= ~Placement;
  31. // 更新
  32. const current = nextEffect.alternate;
  33. commitWork(current, nextEffect);
  34. break;
  35. }
  36. // SSR
  37. case Hydrating: {
  38. nextEffect.effectTag &= ~Hydrating;
  39. break;
  40. }
  41. // SSR
  42. case HydratingAndUpdate: {
  43. nextEffect.effectTag &= ~Hydrating;
  44. const current = nextEffect.alternate;
  45. commitWork(current, nextEffect);
  46. break;
  47. }
  48. // 更新DOM
  49. case Update: {
  50. const current = nextEffect.alternate;
  51. commitWork(current, nextEffect);
  52. break;
  53. }
  54. // 删除DOM
  55. case Deletion: {
  56. commitDeletion(root, nextEffect, renderPriorityLevel);
  57. break;
  58. }
  59. }
  60. nextEffect = nextEffect.nextEffect;
  61. }
  62. }

commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:

  1. 根据ContentReset effectTag重置文字节点
  2. 更新ref
  3. 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating)

我们关注步骤三中的Placement | Update | DeletionHydrating作为服务端渲染相关,我们先不关注。

Placement effect

Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。

调用的方法为commitPlacement

你可以在这里mutation阶段 - 图2 (opens new window)看到commitPlacement源码

该方法所做的工作分为三步:

  1. 获取父级DOM节点。其中finishedWork为传入的Fiber节点
  1. const parentFiber = getHostParentFiber(finishedWork);
  2. // 父级DOM节点
  3. const parentStateNode = parentFiber.stateNode;
  1. 获取Fiber节点DOM兄弟节点
  1. const before = getHostSibling(finishedWork);
  1. 根据DOM兄弟节点是否存在决定调用parentNode.insertBeforeparentNode.appendChild执行DOM插入操作。
  1. // parentStateNode是否是rootFiber
  2. if (isContainer) {
  3. insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  4. } else {
  5. insertOrAppendPlacementNode(finishedWork, before, parent);
  6. }

值得注意的是,getHostSibling(获取兄弟DOM节点)的执行很耗时,当在同一个父Fiber节点下依次执行多个插入操作,getHostSibling算法的复杂度为指数级。

这是由于Fiber节点不只包括HostComponent,所以Fiber树和渲染的DOM树节点并不是一一对应的。要从Fiber节点找到DOM节点很可能跨层级遍历。

考虑如下例子:

  1. function Item() {
  2. return <li><li>;
  3. }
  4. function App() {
  5. return (
  6. <div>
  7. <Item/>
  8. </div>
  9. )
  10. }
  11. ReactDOM.render(<App/>, document.getElementById('root'));

对应的Fiber树DOM树结构为:

  1. // Fiber树
  2. child child child child
  3. rootFiber -----> App -----> div -----> Item -----> li
  4. // DOM树
  5. #root ---> div ---> li

当在div的子节点Item前插入一个新节点p,即App变为:

  1. function App() {
  2. return (
  3. <div>
  4. <p></p>
  5. <Item/>
  6. </div>
  7. )
  8. }

对应的Fiber树DOM树结构为:

  1. // Fiber树
  2. child child child
  3. rootFiber -----> App -----> div -----> p
  4. | sibling child
  5. | -------> Item -----> li
  6. // DOM树
  7. #root ---> div ---> p
  8. |
  9. ---> li

此时DOM节点 p的兄弟节点为li,而Fiber节点 p对应的兄弟DOM节点为:

  1. fiberP.sibling.child

fiber p兄弟fiber Item子fiber li

Update effect

Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。

你可以在这里mutation阶段 - 图3 (opens new window)看到commitWork源码

这里我们主要关注FunctionComponentHostComponent

FunctionComponent mutation

fiber.tagFunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。

你可以在这里mutation阶段 - 图4 (opens new window)看到commitHookEffectListUnmount源码

所谓“销毁函数”,见如下例子:

  1. useLayoutEffect(() => {
  2. // ...一些副作用逻辑
  3. return () => {
  4. // ...这就是销毁函数
  5. }
  6. })

你不需要很了解useLayoutEffect,我们会在下一节详细介绍。你只需要知道在mutation阶段会执行useLayoutEffect的销毁函数。

HostComponent mutation

fiber.tagHostComponent,会调用commitUpdate

你可以在这里mutation阶段 - 图5 (opens new window)看到commitUpdate源码

最终会在updateDOMPropertiesmutation阶段 - 图6 (opens new window)中将render阶段 completeWorkmutation阶段 - 图7 (opens new window)中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

  1. for (let i = 0; i < updatePayload.length; i += 2) {
  2. const propKey = updatePayload[i];
  3. const propValue = updatePayload[i + 1];
  4. // 处理 style
  5. if (propKey === STYLE) {
  6. setValueForStyles(domElement, propValue);
  7. // 处理 DANGEROUSLY_SET_INNER_HTML
  8. } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
  9. setInnerHTML(domElement, propValue);
  10. // 处理 children
  11. } else if (propKey === CHILDREN) {
  12. setTextContent(domElement, propValue);
  13. } else {
  14. // 处理剩余 props
  15. setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
  16. }
  17. }

Deletion effect

Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion

你可以在这里mutation阶段 - 图8 (opens new window)看到commitDeletion源码

该方法会执行如下操作:

  1. 递归调用Fiber节点及其子孙Fiber节点fiber.tagClassComponentcomponentWillUnmountmutation阶段 - 图9 (opens new window)生命周期钩子,从页面移除Fiber节点对应DOM节点
  2. 解绑ref
  3. 调度useEffect的销毁函数

总结

从这节我们学到,mutation阶段会遍历effectList,依次执行commitMutationEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber