前言
上一篇咱們講了 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…
(完)