前言
上一篇咱们讲了 Commit第一子阶段「before mutation」,本篇讲第二子阶段「mutation
」:javascript
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()
做用:
提交HostComponent
的side 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
的指向置为null
github
源码以下: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()
③ 若是effectTag
是PlacementAndUpdate
的话,则针对该节点及子节点进行插入和更新操做,执行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
想插入到Div1
和Div2
之间,那么它的后一个节点就是Div2
;
若是Div4
想插入到Div2
和Div3
之间,那么它的后一个节点就是Div3
;
若是 Div3 是一个组件的话:
若是Div5
想插入到Div2
和Div3Component
之间,那么本质上是插入到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);
复制代码
若是变量before
为null
,则表示插入的位置没有兄弟节点,则执行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()
的递归逻辑
GitHubcommitMutationEffects()
:
github.com/AttackXiaoJ…
commitPlacement()
/getHostParentFiber()
/getHostSibling()
:
github.com/AttackXiaoJ…
(完)