React源碼解析之Commit第二子階段「mutation」(上)

前言
上一篇咱們講了 Commit第一子階段「before mutation」,本篇講第二子階段mutationjavascript

do {
      if (__DEV__) {
        invokeGuardedCallback(null, commitMutationEffects, null);
        //刪除了 dev 代碼
      } else {
        try {
          //提交HostComponent的 side effect,也就是 DOM 節點的操做(增刪改)
          commitMutationEffects();
        } catch (error) {
          invariant(nextEffect !== null'Should be working on an effect.');
          captureCommitPhaseError(nextEffect, error);
          nextEffect = nextEffect.nextEffect;
        }
      }
    } while (nextEffect !== null);
複製代碼

1、commitMutationEffects()
做用:
提交HostComponentside effect,也就是DOM節點的操做(增刪改)php

源碼:html

function commitMutationEffects() {
  // TODO: Should probably move the bulk of this function to commitWork.
  //循環 effect 鏈
  while (nextEffect !== null) {
    setCurrentDebugFiberInDEV(nextEffect);

    const effectTag = nextEffect.effectTag;
    //若是有文字節點,則將value 置爲''
    if (effectTag & ContentReset) {
      commitResetTextContent(nextEffect);
    }
    ////將 ref 的指向置爲 null
    if (effectTag & Ref) {
      const current = nextEffect.alternate;
      if (current !== null) {
        commitDetachRef(current);
      }
    }

    // The following switch statement is only concerned about placement,
    // updates, and deletions. To avoid needing to add a case for every possible
    // bitmap value, we remove the secondary effects from the effect tag and
    // switch on that value.
    //如下狀況是針對 替換(Placement)、更新(Update)和 刪除(Deletion) 的 effectTag 的
    let primaryEffectTag = effectTag & (Placement | Update | Deletion);
    switch (primaryEffectTag) {
      //插入新節點
      case Placement: {
        //針對該節點及子節點進行插入操做
        commitPlacement(nextEffect);
        // Clear the "placement" from effect tag so that we know that this is
        // inserted, before any life-cycles like componentDidMount gets called.
        // TODO: findDOMNode doesn't rely on this any more but isMounted does
        // and isMounted is deprecated anyway so we should be able to kill this.
        nextEffect.effectTag &= ~Placement;
        break;
      }
      case PlacementAndUpdate: {
        // Placement
        //針對該節點及子節點進行插入操做
        commitPlacement(nextEffect);
        // Clear the "placement" from effect tag so that we know that this is
        // inserted, before any life-cycles like componentDidMount gets called.
        nextEffect.effectTag &= ~Placement;

        // Update
        const current = nextEffect.alternate;
        //對 DOM 節點上的屬性進行更新
        commitWork(current, nextEffect);
        break;
      }
      //更新節點
      //舊節點->新節點
      case Update: {
        const current = nextEffect.alternate;
        //對 DOM 節點上的屬性進行更新
        commitWork(current, nextEffect);
        break;
      }
      case Deletion: {
        //刪除節點
        commitDeletion(nextEffect);
        break;
      }
    }

    // TODO: Only record a mutation effect if primaryEffectTag is non-zero.
    //不看
    recordEffect();
    //dev,不看
    resetCurrentDebugFiberInDEV();
    nextEffect = nextEffect.nextEffect;
  }
}
複製代碼

解析:
循環effect鏈,進行如下操做:java

(1) 若是是文字節點,即effectTag裏包含ContentReset的話,執行commitResetTextContent(),將文本值置爲 '' node

源碼以下:
commitResetTextContent()react

//重置文字內容
function commitResetTextContent(current: Fiber{
  if (!supportsMutation) {
    return;
  }
  resetTextContent(current.stateNode);
}
複製代碼

resetTextContent()nginx

//將該 DOM 節點的 value 設置爲 ''
export function resetTextContent(domElement: Instance): void {
  //給 DOM 節點設置text
  setTextContent(domElement, '');
}
複製代碼

setTextContent()git

//給 DOM 節點設置text
let setTextContent = function(node: Element, text: string): void {
  if (text) {
    let firstChild = node.firstChild;
    //若是隻有一個子節點且是文字節點,將其value置爲 text
    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  //text 爲'',則直接執行這一步
  node.textContent = text;
};
複製代碼

(2) 若是有設置ref的話,即effectTag裏包含Ref的話,執行commitDetachRef(),將ref 的指向置爲nullgithub

源碼以下:
commitDetachRef()web

//將 ref 的指向置爲 null
function commitDetachRef(current: Fiber{
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      currentRef(null);
    } else {
      currentRef.current = null;
    }
  }
}
複製代碼

(3) 若是effectTag包含增改刪的話,則根據不一樣的狀況進行不一樣的操做

① 注意下這種寫法:

  let primaryEffectTag = effectTag & (Placement | Update | Deletion);
複製代碼

先是Placement(替換/新增)、Update(更新) 和Deletion(刪除) 三者之間的操做,至關於把三者合併在了一塊兒。

而後將其和effectTag進行操做,從而獲得不一樣的集合,如「增/刪/改」和「增改」

② 若是effectTag只是Placement的話,則針對該節點及子節點進行插入操做,執行commitPlacement()

③ 若是effectTagPlacementAndUpdate的話,則針對該節點及子節點進行插入和更新操做,執行commitPlacement()commitWork()

由於該狀況是 ② 和 ④ 的集合,因此會跳過,詳細講完 ② 和 ④ 後,想必這邊你也知道了。

④ 若是effectTag只是Update的話,則針對該節點及子節點進行更新操做,執行commitWork()

⑤ 若是effectTag只是Deletion的話,則針對該節點及子節點進行刪除節點操做,執行commitDeletion()

CUD操做結束後,移到下一個 effect,循環以上操做:

  nextEffect = nextEffect.nextEffect;
複製代碼

接下來這個很重要,由於是貫穿 ②、④、⑤ 中的算法——深度優先遍歷算法,看懂後,相信也不難理解 ②、④、⑤ 的源碼邏輯。

2、ReactDOM裏的深度優先遍歷
概念:
寫了幾遍發現寫不清楚,直接看下面的僞代碼和講解吧。

僞代碼:

  let node=Div1
  while (true) {
    //node.child 表示子節點
    if (node.child !== null) {
      //return 表示父節點
      node.child.return = node;
      //到子節點
      node = node.child;
      continue;
    }
    //沒有子節點時
    else if (node.child === null) {
      //當沒有兄弟節點時
      while (node.sibling === null) {
        //父節點爲 null 或者 父節點是 Div1
        if (node.return === null || node.return === Div1) {
          // 跳出最外面的while循環
          return
        }
        //到父節點
        node = node.return;
      }
      //兄弟節點的 return 也是父節點
      node.sibling.return = node.return;
      //移到兄弟節點,再次循環
      node = node.sibling;
      continue
    }
  }
複製代碼

fiber 樹:

講解:
看圖來遍歷下這棵樹

① node 表示當前遍歷的節點,目前爲 Div1
② Div1.child 有值爲 Div2(將其賦給 node)
③ Div2.child 有值爲 Div3(將其賦給 node)
④ Div3.child 沒有值,判斷 Div3.sibling 是否有值
⑤ Div3.sibling 有值爲 Div4(將其賦給 node),判斷 Div4.child 是否有值
⑥ Div4.child 有值爲 Div5(將其賦給 node)
⑦ Div5.child 沒有值,判斷 Div5.sibling 是否有值
⑧ Div5.sibling 沒有值,則 Div5.return,返回至父節點 Div4(將其賦給 node),判斷 Div4.sibling 是否有值
⑨ Div4.sibling 沒有值,則 Div4.return,返回至父節點 Div2(將其賦給 node),判斷 Div2.sibling 是否有值
⑩ Div2.sibling 有值爲 Div6(將其賦給 node),判斷 Div6.child 是否有值
⑪ Div6.child 有值爲 Div7(將其賦給 node)
⑫ Div7.child 沒有值,判斷 Div7.sibling 是否有值
⑬ Div7.sibling 沒有值,則 Div7.return,返回至父節點 Div6(將其賦給 node),判斷 Div6.sibling 是否有值
⑭ Div6.sibling 沒有值,則 Div6.return,返回至父節點 Div1(將其賦給 node),判斷 Div1.sibling 是否有值
⑮ Div1.sibling 沒有值,而且 Div1.return 爲 null,而且 Div1 就是一開始的節點,因此,到此樹遍歷結束。

相信看完上述過程,你確定知道其中有重複的邏輯,也就是遞歸邏輯,綜合僞代碼,相信你已經明白了 ReactDOM 進行插入、更新、刪除進行的 fiber 樹遍歷邏輯

3、commitPlacement()
做用:
針對該節點及子節點進行插入操做

源碼:

function commitPlacement(finishedWork: Fiber)void {
  if (!supportsMutation) {
    return;
  }

  // Recursively insert all host nodes into the parent.
  //向上循環祖先節點,返回是 DOM 元素的父節點
  const parentFiber = getHostParentFiber(finishedWork);

  // Note: these two variables *must* always be updated together.
  let parent;
  let isContainer;
  //判斷父節點的類型
  switch (parentFiber.tag) {
    //若是是 DOM 元素的話
    case HostComponent:
      //獲取對應的 DOM 節點
      parent = parentFiber.stateNode;
      isContainer = false;
      break;
    //若是是 fiberRoot 節點的話,
    //關於 fiberRoot ,請看:[React源碼解析之FiberRoot](https://mp.weixin.qq.com/s/AYzNSoMXEFR5XC4xQ3L8gA)
    case HostRoot:
      parent = parentFiber.stateNode.containerInfo;
      isContainer = true;
      break;
    //React.createportal 節點的更新
    //https://zh-hans.reactjs.org/docs/react-dom.html#createportal
    case HostPortal:
      parent = parentFiber.stateNode.containerInfo;
      isContainer = true;
      break;
    default:
      invariant(
        false,
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
  //若是父節點是文本節點的話
  if (parentFiber.effectTag & ContentReset) {
    // Reset the text content of the parent before doing any insertions
    //在進行任何插入操做前,須要先將 value 置爲 ''
    resetTextContent(parent);
    // Clear ContentReset from the effect tag
    //再清除掉 ContentReset 這個 effectTag
    parentFiber.effectTag &= ~ContentReset;
  }
  //查找插入節點的位置,也就是獲取它後一個 DOM 兄弟節點的位置
  const before = getHostSibling(finishedWork);
  // We only have the top Fiber that was inserted but we need to recurse down its
  // children to find all the terminal nodes.
  //循環,找到全部子節點
  let node: Fiber = finishedWork;
  while (true) {
    //若是待插入的節點是一個 DOM 元素的話
    if (node.tag === HostComponent || node.tag === HostText) {
      //獲取 fiber 節點對應的 DOM 元素
      const stateNode = node.stateNode;
      //找到了待插入的位置,好比 before 是 div,就表示在 div 的前面插入 stateNode
      if (before) {
        //父節點不是 DOM 元素的話
        if (isContainer) {
          insertInContainerBefore(parent, stateNode, before);
        }
        //父節點是 DOM 元素的話,執行DOM API--insertBefore()
        //https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore
        else {
          //parentInstance.insertBefore(child, beforeChild);
          insertBefore(parent, stateNode, before);
        }
      }
      //插入的是節點是沒有兄弟節點的話,執行 appendChild
      //https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
      else {
        if (isContainer) {
          appendChildToContainer(parent, stateNode);
        } else {
          appendChild(parent, stateNode);
        }
      }
    } else if (node.tag === HostPortal) {
      // If the insertion itself is a portal, then we don't want to traverse
      // down its children. Instead, we'll get insertions from each child in
      // the portal directly.
    }
    //若是是組件節點的話,好比 ClassComponent,則找它的第一個子節點(DOM 元素),進行插入操做
    else if (node.child !== null) {
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === finishedWork) {
      return;
    }
    //若是待插入的節點是 ClassComponent 或 FunctionComponent 的話,還要執行內部節點的插入操做
    //也就是說組件內部可能還有多個子組件,也是要循環插入的

    //當沒有兄弟節點,也就是目前的節點是最後一個節點的話
    while (node.sibling === null) {
      //循環週期結束,返回到了最初的節點上,則插入操做已經所有結束
      if (node.return === null || node.return === finishedWork) {
        return;
      }
      //從下至上,從左至右,查找要插入的兄弟節點
      node = node.return;
    }
    //移到兄弟節點,判斷是不是要插入的節點,一直循環
    node.sibling.return = node.return;
    node = node.sibling;
  }
}
複製代碼

解析:
(1) 執行getHostParentFiber(),獲取待插入節點的 DOM 類型的祖先節點

源碼以下:
getHostParentFiber()

//向上循環祖先節點,返回是 DOM 元素的父節點
function getHostParentFiber(fiber: Fiber)Fiber {
  let parent = fiber.return;
  //向上循環祖先節點,返回是 DOM 元素的父節點
  while (parent !== null) {
    //父節點是 DOM 元素的話,返回其父節點
    if (isHostParent(parent)) {
      return parent;
    }
    parent = parent.return;
  }
  invariant(
    false,
    'Expected to find a host parent. This error is likely caused by a bug ' +
      'in React. Please file an issue.',
  );
}
複製代碼

isHostParent()

//判斷目標節點是不是 DOM 節點
function isHostParent(fiber: Fiber): boolean {
  return (
    fiber.tag === HostComponent ||
    fiber.tag === HostRoot ||
    fiber.tag === HostPortal
  );
}
複製代碼

(2) 而後是判斷祖先節點parentFiber的類型,咱們只看HostComponent,便是 DOM 元素的狀況,目的就是拿到祖先節點對應的 DOM 節點—parent,並將isContainer設爲false,爲下面的邏輯作鋪墊。

(3) 若是父節點是文本節點的話,則執行resetTextContent(),清空文本值

源碼以下:
resetTextContent()

//將該 DOM 節點的 value 設置爲 ''
export function resetTextContent(domElement: Instance): void {
  //給 DOM 節點設置text
  setTextContent(domElement, '');
}
複製代碼

setTextContent()

//給 DOM 節點設置text
let setTextContent = function(node: Element, text: string): void {
  if (text) {
    let firstChild = node.firstChild;
    //若是隻有一個子節點且是文字節點,將其value置爲 text
    if (
      firstChild &&
      firstChild === node.lastChild &&
      firstChild.nodeType === TEXT_NODE
    ) {
      firstChild.nodeValue = text;
      return;
    }
  }
  //text 爲'',則直接執行這一步
  node.textContent = text;
};
複製代碼

我想了想,開發層面上,好像沒有遇到父節點是文本節點的狀況,因此也找不到具體的樣例,若是有同窗知道的話,麻煩留言。

(4) 執行getHostSibling(),查找插入節點的位置,也就是獲取它後一個 DOM 兄弟節點的位置

舉個例子:

假定有三個Div如上圖所示。
若是Div4想插入到Div1Div2之間,那麼它的後一個節點就是Div2
若是Div4想插入到Div2Div3之間,那麼它的後一個節點就是Div3

若是 Div3 是一個組件的話:

若是Div5想插入到Div2Div3Component之間,那麼本質上是插入到Div2和Div4之間,因此它的後一節點是Div4

好,知道了上面的插入邏輯後,咱們再來看getHostSibling()的源碼:

getHostSibling()

//查找插入節點的位置,也就是獲取它後一個 DOM 兄弟節點的位置
//好比:在ab上,插入 c,插在 b 以前,找到兄弟節點 b;插在 b 以後,無兄弟節點
function getHostSibling(fiber: Fiber): ?Instance {
  // We're going to search forward into the tree until we find a sibling host
  // node. Unfortunately, if multiple insertions are done in a row we have to
  // search past them. This leads to exponential search for the next sibling.
  // TODO: Find a more efficient way to do this.
  let node: Fiber = fiber;
  //將外部 while 循環命名爲 siblings,以便和內部 while 循環區分開
  siblings: while (true) {
    // If we didn't find anything, let's try the next sibling.
    //從目標節點向上循環,若是該節點沒有兄弟節點,而且 父節點爲 null 或是 父節點是DOM 元素的話,跳出循環

    //例子:樹
    //     a
    //    /
    //   b
    // 在 a、b之間插入 c,那麼 c 是沒有兄弟節點的,直接返回 null
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        // If we pop out of the root or hit the parent the fiber we are the
        // last sibling.
        return null;
      }
      node = node.return;
    }
    //node 的兄弟節點的 return 指向 node 的父節點
    node.sibling.return = node.return;
    //移到兄弟節點上
    node = node.sibling;
    //若是 node.silbing 不是 DOM 元素的話(便是一個組件)
    //查找(node 的兄弟節點)(node.sibling) 中的第一個 DOM 節點
    while (
      node.tag !== HostComponent &&
      node.tag !== HostText &&
      node.tag !== DehydratedSuspenseComponent
    ) {
      // If it is not host node and, we might have a host node inside it.
      // Try to search down until we find one.
      //嘗試在非 DOM 節點內,找到 DOM 節點

      //跳出本次 while 循環,繼續siblings while 循環
      if (node.effectTag & Placement) {
        // If we don't have a child, try the siblings instead.
        continue siblings;
      }
      // If we don't have a child, try the siblings instead.
      // We also skip portals because they are not part of this host tree.
      //若是 node 沒有子節點,則從兄弟節點查找
      if (node.child === null || node.tag === HostPortal) {
        continue siblings;
      }
      //循環子節點
      //找到兄弟節點上的第一個 DOM 節點
      else {
        node.child.return = node;
        node = node.child;
      }
    }
    // Check if this host node is stable or about to be placed.
    //找到了要插入的 node 的兄弟節點是一個 DOM 元素,而且它不是新增的節點的話,
    //返回該節點,也就是說找到了要插入的節點的位置,即在該節點的前面
    if (!(node.effectTag & Placement)) {
      // Found it!
      return node.stateNode;
    }
  }
}
複製代碼

① 先講一個知識點:給while循環命名,以便和內部的while循環區分開

  let a=5

  while1:while(a>0){
    a=a-1
    console.log(a,'while1')

    while(a>=3){
      console.log(a,'innerWhile2')
      //跳過本次循環,繼續執行循環 while1
      continue while1
    }
    while(a<3){
      console.log(a,'innerWhile1')
      //跳過本次循環,繼續執行循環 while1
      continue while1
    }

  }
複製代碼

getHostSibling()的查找成功的邏輯是:

[1] 優先查找待插入節點的兄弟節點,若是兄弟節點存在,而且該兄弟節點不是組件類型的節點,也不是新增的節點的話,則找到了待插入的位置,即在兄弟節點以前插入,而後跳出siblings-while循環

[2] 優先查找待插入節點的兄弟節點,若是兄弟節點存在,而且該兄弟節點是組件類型的節點(好比 ClassComponent),也不是新增節點的話,則找組件節點的第一個是 DOM 元素的子節點,此時就找到了待插入的位置,即在組件節點的第一個DOM類型子節點以前插入,而後跳出siblings-while循環

(5) 好,此時 變量before的值要麼是一個 DOM 實例,要麼是 null

接下來只考慮待插入節點是 DOM 節點且isContainer = false的話,則進入到下面的判斷:

if (node.tag === HostComponent || node.tag === HostText){ }
複製代碼

獲取待插入 fiber 對象的 DOM 實例,
若是變量before存在,則找到了兄弟節點,執行insertBefore(),將其插入到兄弟節點以前:

  //源碼:parentInstance.insertBefore(child, beforeChild);
  insertBefore(parent, stateNode, before);
複製代碼

若是變量beforenull,則表示插入的位置沒有兄弟節點,則執行appendChild(),將其插入到末尾節點以後:

  //源碼:parentInstance.appendChild(child);
  appendChild(parent, stateNode);
複製代碼

若是待插入節點是一個ClassComponent這樣的組件節點的話,則找它的第一個 DOM 類型的子節點或者是第一個 DOM 類型的兄弟節點進行插入,最後一段是組件類型的節點及其子節點進行遞歸插入的邏輯。

4、後續
因爲篇幅和精力緣由,DOM 節點更新操做——commitWork()和 DOM 節點刪除操做——commitDeletion(),放在下篇講。

總結
經過本文,你須要知道:
(1) effectTag & (Placement | Update | Deletion)的意思
(2) ReactDOM 裏的深度優先遍歷算法
(3) 查找待插入節點的兄弟節點的位置的方法——getHostSibling()的邏輯
(4) commit階段,進行真實 DOM 節點插入的方法——commitPlacement()的遞歸邏輯

GitHub
commitMutationEffects()
github.com/AttackXiaoJ…

commitPlacement()/getHostParentFiber()/getHostSibling()
github.com/AttackXiaoJ…


(完)

相關文章
相關標籤/搜索