首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之Commit第二子阶段「mutation」(上)

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

作者头像
进击的小进进
发布2020-04-14 15:42:38
1K0
发布2020-04-14 15:42:38
举报

前言

上一篇我们讲了 Commit第一子阶段「before mutation」,本篇讲第二子阶段 mutation

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);

一、commitMutationEffects()

作用: 提交HostComponentside effect,也就是DOM节点的操作(增删改)

源码:

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链,进行以下操作:

(1) 如果是文字节点,即effectTag里包含ContentReset的话,执行commitResetTextContent(),将文本值置为 ''

源码如下: commitResetTextContent()

//重置文字内容
function commitResetTextContent(current: Fiber) {
  if (!supportsMutation) {
    return;
  }
  resetTextContent(current.stateNode);
}

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;
};

(2) 如果有设置ref的话,即effectTag里包含Ref的话,执行commitDetachRef(),将ref 的指向置为null

源码如下: commitDetachRef()

//将 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;

接下来这个很重要,因为是贯穿 ②、④、⑤ 中的算法——深度优先遍历算法,看懂后,相信也不难理解 ②、④、⑤ 的源码逻辑。

二、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 树遍历逻辑

三、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 类型的兄弟节点进行插入,最后一段是组件类型的节点及其子节点进行递归插入的逻辑。

四、后续

由于篇幅和精力原因,DOM 节点更新操作——commitWork()和 DOM 节点删除操作——commitDeletion(),放在下篇讲。

总结

通过本文,你需要知道: (1) effectTag & (Placement | Update | Deletion)的意思 (2) ReactDOM 里的深度优先遍历算法 (3) 查找待插入节点的兄弟节点的位置的方法——getHostSibling()的逻辑 (4) commit阶段,进行真实 DOM 节点插入的方法——commitPlacement()的递归逻辑

GitHub

commitMutationEffects()

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js

commitPlacement()/getHostParentFiber()/getHostSibling()

https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、commitMutationEffects()
  • 二、ReactDOM里的深度优先遍历
  • 三、commitPlacement()
  • 四、后续
  • 总结
  • GitHub
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档