image.png

commitMutationEffects

commitMutationEffects是一个while循环,遍历包含effect tag的fiber节点的链表。
遍历的每一个Fiber节点:

  1. 会判断是否包含ContentReset,即是否需要重置文本节点。
  2. 是否包含Ref

image.png

  1. 判断是否包含Placement(插入dom)、Update(更新)、Delection(删除)、Hydrating(ssr)相关
    • Placement,执行commitPlacement(nextEffect)
  • 当前环境不支持mutation,则直接返回。

image.png

placement

commitPlacement

  1. function commitPlacement(finishedWork) {
  2. var parentFiber = getHostParentFiber(finishedWork); // Note: these two variables *must* always be updated together.
  3. switch (parentFiber.tag) {
  4. case HostComponent:
  5. {
  6. var parent = parentFiber.stateNode;
  7. if (parentFiber.flags & ContentReset) {
  8. // Reset the text content of the parent before doing any insertions
  9. resetTextContent(parent); // Clear ContentReset from the effect tag
  10. parentFiber.flags &= ~ContentReset;
  11. }
  12. var before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need to recurse down its
  13. // children to find all the terminal nodes.
  14. insertOrAppendPlacementNode(finishedWork, before, parent);
  15. break;
  16. }
  17. case HostRoot:
  18. case HostPortal:
  19. {
  20. var _parent = parentFiber.stateNode.containerInfo;
  21. var _before = getHostSibling(finishedWork);
  22. insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
  23. break;
  24. }
  25. // eslint-disable-next-line-no-fallthrough
  26. default:
  27. throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.');
  28. }
  29. }

根据当前fiber节点,找到离他最近的Host Fiber节点,包括HostComponent、HostRoot、HostPortal(通过React.createHostPorta传进来的),这几种类型共同点:都对应有dom节点。
当找到parentFiber时,如果parentFiber包含ContentReset的Effect tag,就需要重置resetTextContent,ContentReset Effect tag并取反(即删除这个effctTag)。
接下来,找getHostSibling,当前Fiber类型的兄弟节点。
为什么要找兄弟节点?
插入一个dom节点有两种方式:一种是sibling.insertBefore方法,另一个时parentNode.append(child)。因此如果使用insertBefore就需要寻找兄弟节点,如果是append,则需要parentNode节点。

  1. function insertOrAppendPlacementNode(node, before, parent) {
  2. var tag = node.tag;
  3. var isHost = tag === HostComponent || tag === HostText;
  4. if (isHost) {
  5. var stateNode = node.stateNode;
  6. //如果当前节点的兄弟节点存在,就执行insertBefore,否则appendChild
  7. if (before) {
  8. insertBefore(parent, stateNode, before);
  9. } else {
  10. appendChild(parent, stateNode);
  11. }
  12. } else if (tag === HostPortal) ; else {
  13. var child = node.child;
  14. if (child !== null) {
  15. insertOrAppendPlacementNode(child, before, parent);
  16. var sibling = child.sibling;
  17. while (sibling !== null) {
  18. insertOrAppendPlacementNode(sibling, before, parent);
  19. sibling = sibling.sibling;
  20. }
  21. }
  22. }
  23. }
  1. //调用Dom的insertBefore方法
  2. function insertBefore(parentInstance, child, beforeChild) {
  3. parentInstance.insertBefore(child, beforeChild);
  4. }
  5. function insertInContainerBefore(container, child, beforeChild) {
  6. if (container.nodeType === COMMENT_NODE) {
  7. container.parentNode.insertBefore(child, beforeChild);
  8. } else {
  9. container.insertBefore(child, beforeChild);
  10. }
  11. }
  12. //调用Dom的appendChild方法
  13. function appendChild(parentInstance, child) {
  14. parentInstance.appendChild(child);
  15. }

getHostParentFiber

getHostParentFiber:一直向上递归查找最近的host类型Fiber节点,知道找到为止。

  1. function getHostParentFiber(fiber) {
  2. var parent = fiber.return;
  3. while (parent !== null) {
  4. if (isHostParent(parent)) {
  5. return parent;
  6. }
  7. parent = parent.return;
  8. }
  9. throw new Error('Expected to find a host parent. This error is likely caused by a bug ' + 'in React. Please file an issue.');
  10. }

getHostSibling

  1. function getHostSibling(fiber) {
  2. // We're going to search forward into the tree until we find a sibling host
  3. // node. Unfortunately, if multiple insertions are done in a row we have to
  4. // search past them. This leads to exponential search for the next sibling.
  5. // TODO: Find a more efficient way to do this.
  6. var node = fiber;
  7. siblings: while (true) {
  8. // If we didn't find anything, let's try the next sibling.
  9. while (node.sibling === null) {
  10. if (node.return === null || isHostParent(node.return)) {
  11. // If we pop out of the root or hit the parent the fiber we are the
  12. // last sibling.
  13. return null;
  14. }
  15. node = node.return;
  16. }
  17. node.sibling.return = node.return;
  18. node = node.sibling;
  19. while (node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedFragment) {
  20. // If it is not host node and, we might have a host node inside it.
  21. // Try to search down until we find one.
  22. if (node.flags & Placement) {
  23. // If we don't have a child, try the siblings instead.
  24. continue siblings;
  25. } // If we don't have a child, try the siblings instead.
  26. // We also skip portals because they are not part of this host tree.
  27. if (node.child === null || node.tag === HostPortal) {
  28. continue siblings;
  29. } else {
  30. node.child.return = node;
  31. node = node.child;
  32. }
  33. } // Check if this host node is stable or about to be placed.
  34. if (!(node.flags & Placement)) {
  35. // Found it!
  36. return node.stateNode;
  37. }
  38. }
  39. }

有两层while循环,原因
image.png
上述Demo,从fiber树角度,一次是FiberRootNode、当前dom的Fiber节点(React.render产生的RootFiber)、RootFiber的子节点依次是App Fiber,div Fiber,Item Fiber,li Fiber。
image.png
从Dom树角度,当前的根节点是#root div,子节点依次是div、li。
可见,dom的结构与Fiber的结构并不对应。
image.png
如果在item前插入p节点,如何插入?dom数中是在li调用insertBefore插入p。所以在Fiber节点中,需要先找到li。但是p对应的兄弟节点不是li,而是Item。Item的子Fiber节点才是li,因此我们要寻找fiber节点的兄弟节点,这个查找可能是跨层级的,因此getHostSibling有两层循环。
当执行玩commitComponent以后,dom节点已经插入dom数中,需要删除Placement。
image.png

PlacementAndUpdate

image.png
要先调用插入的commitPlacement,再调update的commitWork。

commitWork

image.png

FunctionComponent

FunctionComponent、ForwardRef、MemoComponent、SimpleComponent、Block,这些节点都与FunctionComponent相关,这些case调用commitHookEffecListUnmount,会调用useLayoutEffect的销毁函数。
image.png
commitHookEffecListUnmount:会遍历effectList,如果出入的effect包含了传入的tag(hookLayout),需要先执行他们的销毁函数,即useLayoutEffect回调函数的返回值所以在执行任何effctTag函数之前,都会执行useLayoutEffect的销毁函数。

HostComponnet

HostComponnet:会调用commitUpdate方法,接受的参数updatePayload,即updateQueue属性(Fiber节点props和children等属性改变的值,第i是key,第i+1项是value)。
image.png
commitUpdate调用UpdateProperties来更新dom的属性
image.png

Deletion

commitDeletion

支持mutation,调用unmoutHostComponents
image.png

unmoutHostComponents

要先删除一个节点,需先找到这个节点的父级节点,
image.png
根据不同的tag,找到父级Dom节点。

  1. findParent: while (parent !== null) {
  2. switch (parent.tag) {
  3. case HostComponent:
  4. {
  5. hostParent = parent.stateNode;
  6. hostParentIsContainer = false;
  7. break findParent;
  8. }
  9. case HostRoot:
  10. {
  11. hostParent = parent.stateNode.containerInfo;
  12. hostParentIsContainer = true;
  13. break findParent;
  14. }
  15. case HostPortal:
  16. {
  17. hostParent = parent.stateNode.containerInfo;
  18. hostParentIsContainer = true;
  19. break findParent;
  20. }
  21. }

HostComponnet
找到父级节点后,执行commitNestUnmounts,嵌套的执行unMount,因为当我们要删除一个Fiber节点时,这个Fiber节点可能是一颗子树,这颗Fiber节点的所有的子、孙节点都需要递归删除。
image.png

删除操作:commitUnmout中

image.png
对于FunctionComponent节点,需要执行enqueuePendingPassiveHookEffectUnmout(注册需要执行的useEffect的回调函数)。所以当FunctionComponent包含useEffect,那么销毁时也会执行useEffect的函数。
image.png
对于ClassComponent时,会执行componentWillUnmout生命周期钩子。
image.png
对于HostComponent,会解绑他的Ref属性。

总结

mutation阶段,会遍历包含effectTag Fiber的链表,判断这些Fiber节点包含的effectTag,分别做对应的操作,包括重置文本节点、解绑Ref或更新Ref、插入Dom、跟新Dom、删除Dom和ssr等相关操作。