上一篇我们讲了 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);
作用:
提交HostComponent
的side 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()
③ 如果effectTag
是PlacementAndUpdate
的话,则针对该节点及子节点进行插入和更新操作,执行commitPlacement()
和commitWork()
因为该情况是 ② 和 ④ 的集合,所以会跳过,详细讲完 ② 和 ④ 后,想必这边你也知道了。
④ 如果effectTag
只是Update
的话,则针对该节点及子节点进行更新操作,执行commitWork()
⑤ 如果effectTag
只是Deletion
的话,则针对该节点及子节点进行删除节点操作,执行commitDeletion()
⑥ CUD
操作结束后,移到下一个 effect,循环以上操作:
nextEffect = nextEffect.nextEffect;
接下来这个很重要,因为是贯穿 ②、④、⑤ 中的算法——深度优先遍历算法,看懂二
后,相信也不难理解 ②、④、⑤ 的源码逻辑。
概念: 写了几遍发现写不清楚,直接看下面的伪代码和讲解吧。
伪代码:
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 树遍历逻辑
作用: 针对该节点及子节点进行插入操作
源码:
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 类型的兄弟节点进行插入,最后一段是组件类型的节点及其子节点进行递归插入的逻辑。
由于篇幅和精力原因,DOM 节点更新操作——commitWork()
和 DOM 节点删除操作——commitDeletion()
,放在下篇讲。
通过本文,你需要知道:
(1) effectTag & (Placement | Update | Deletion)
的意思
(2) ReactDOM 里的深度优先遍历算法
(3) 查找待插入节点的兄弟节点的位置的方法——getHostSibling()
的逻辑
(4) commit阶段,进行真实 DOM 节点插入的方法——commitPlacement()
的递归逻辑
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