前言
在上篇文章 React源碼解析之Commit第二子階段「mutation」(中) 中,咱們講了「mutation
」子階段的更新(Update
)操做,接下來咱們講刪除(Deletion
)操做:javascript
case Deletion: {
//刪除節點
commitDeletion(nextEffect);
break;
}
複製代碼
1、commitDeletion()
做用:
刪除 DOM 節點php
源碼:java
function commitDeletion(current: Fiber): void {
//由於是 DOM 操做,因此supportsMutation爲 true
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
//刪除該節點的時候,還會刪除子節點
//若是子節點是 ClassComponent 的話,須要執行生命週期 API——componentWillUnmount()
unmountHostComponents(current);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
//卸載 ref
commitNestedUnmounts(current);
}
//重置 fiber 屬性
detachFiber(current);
}
複製代碼
解析:
(1) 執行unmountHostComponents()
,刪除目標節點及其子節點,若是目標節點或子節點是類組件ClassComponent
的話,會執行內部的生命週期 API——componentWillUnmount()
node
(2) 執行detachFiber()
,重置fiber
屬性react
detachFiber()
的源碼以下:git
//重置 fiber 對象,釋放內存(注意是屬性值置爲 null,不會刪除屬性)
function detachFiber(current: Fiber) {
// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
//重置目標 fiber對象,理想狀況下,也應該清除父 fiber的指向(該 fiber),這樣有利於垃圾回收
//可是 React肯定不了父節點,因此會在目標 fiber 下生成一個子 fiber,表明垃圾回收,該子節點
//會在父節點更新的時候,成爲垃圾回收
current.return = null;
current.child = null;
current.memoizedState = null;
current.updateQueue = null;
current.dependencies = null;
const alternate = current.alternate;
//使用的doubleBuffer技術,Fiber在更新後,不用再從新建立對象,而是複製自身,而且二者相互複用,用來提升性能
//至關因而當前 fiber 的一個副本,用來節省內存用的,也要清空屬性
if (alternate !== null) {
alternate.return = null;
alternate.child = null;
alternate.memoizedState = null;
alternate.updateQueue = null;
alternate.dependencies = null;
}
}
複製代碼
接下來看下unmountHostComponents()
github
2、unmountHostComponents()
做用:
刪除目標節點及其子節點,若是目標節點或子節點是類組件ClassComponent
的話,會執行內部的生命週期 API——componentWillUnmount()
web
源碼:算法
function unmountHostComponents(current): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;
// Each iteration, currentParent is populated with node's host parent if not
// currentParentIsValid.
let currentParentIsValid = false;
// Note: these two variables *must* always be updated together.
let currentParent;
let currentParentIsContainer;
//從上至下,遍歷兄弟節點、子節點
while (true) {
if (!currentParentIsValid) {
//獲取父節點
let parent = node.return;
//將此 while 循環命名爲 findParent
//此循環的目的是找到是 DOM 類型的父節點
findParent: while (true) {
invariant(
parent !== null,
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
switch (parent.tag) {
case HostComponent:
//獲取父節點對應的 DOM 元素
currentParent = parent.stateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
}
parent = parent.return;
}
//執行到這邊,說明找到了符合條件的父節點
currentParentIsValid = true;
}
//若是是 DOM 元素或文本元素的話(主要看這個)
if (node.tag === HostComponent || node.tag === HostText) {
//在目標節點被刪除前,從該節點開始深度優先遍歷,卸載 ref 和執行 componentWillUnmount()/effect.destroy()
commitNestedUnmounts(node);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
//咱們只看 false 的狀況,也就是操做 DOM 標籤的狀況
if (currentParentIsContainer) {
removeChildFromContainer(
((currentParent: any): Container),
(node.stateNode: Instance | TextInstance),
);
}
else {
//源碼:parentInstance.removeChild(child);
removeChild(
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
}
// Don't visit children because we already visited them.
}
//suspense 組件不看
else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedSuspenseComponent
) {
//不看這部分
}
//portal 不看
else if (node.tag === HostPortal) {
//不看這部分
}
//上述狀況都不符合,多是一個 Component 組件
else {
//卸載 ref 和執行 componentWillUnmount()/effect.destroy()
commitUnmount(node);
// Visit children because we may find more host components below.
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
//子樹已經遍歷完
if (node === current) {
return;
}
while (node.sibling === null) {
//若是遍歷回頂點 或 遍歷完子樹,則直接 return
if (node.return === null || node.return === current) {
return;
}
//不然向上遍歷,向兄弟節點遍歷
node = node.return;
if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}
// 向上遍歷,向兄弟節點遍歷
node.sibling.return = node.return;
node = node.sibling;
}
}
複製代碼
解析:
咱們仍是隻考慮HostComponent
和ClassCpmonent
的狀況,該方法也是一個深度優先遍歷的算法邏輯,因此你必須知道該算法邏輯,才能看得懂while (true) { }
裏面作了什麼。數組
關於「ReactDOM裏的深度優先遍歷」請看:
React源碼解析之Commit第二子階段「mutation」(上)中的「2、ReactDOM裏的深度優先遍歷」
優先遍歷子節點,而後再遍歷兄弟節點
(1) 若是當前節點是DOM 標籤HostComponent
或文本節點HostText
的話
if (node.tag === HostComponent || node.tag === HostText) {
複製代碼
① 執行commitNestedUnmounts()
commitNestedUnmounts(node);
複製代碼
commitNestedUnmounts()
的做用是:
在目標節點被刪除前,從該節點開始深度優先遍歷,卸載ref
和執行 componentWillUnmount()/effect.destroy()
注意:commitNestedUnmounts()
方法,不會執行removeChild()
刪除節點的操做
② 執行removeChild()
,刪除當前節點
removeChild(
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
複製代碼
removeChild()
的源碼以下:
export function removeChild(
parentInstance: Instance,
child: Instance | TextInstance | SuspenseInstance,
): void {
parentInstance.removeChild(child);
}
複製代碼
就是調用 DOM API——removeChild,請參考:
developer.mozilla.org/zh-CN/docs/…
(2) 若是當前節點是類組件ClassComponent
或函數組件FunctionComponent
的話(也就是最後的 else 狀況),則執行commitUnmount()
,卸載ref
和執行componentWillUnmount()/effect.destroy()
else {
//卸載 ref 和執行 componentWillUnmount()/effect.destroy()
commitUnmount(node);
// Visit children because we may find more host components below.
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
複製代碼
而後就是一直循環,直到調用return
,跳出無限循環。
unmountHostComponents()
的邏輯其實和commitPlacement()
相似,關於commitPlacement()
,請看:
React源碼解析之Commit第二子階段「mutation」(上)
接下來,咱們講下commitNestedUnmounts()
和commitUnmount()
源碼
3、commitNestedUnmounts()
做用:
深度優先遍歷,循環執行:
在目標節點被刪除前,從該節點開始深度優先遍歷,卸載該節點及其子節點 ref 和執行該節點及其子節點 componentWillUnmount()/effect.destroy()
源碼:
function commitNestedUnmounts(root: Fiber): void {
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
//當在被刪除的目標節點的內部時,咱們不想在內部調用removeChild,由於子節點會被父節點給統一刪除
//可是 React 要在目標節點被刪除的時候,執行componentWillUnmount,這就是commitNestedUnmounts的目的
let node: Fiber = root;
while (true) {
// 卸載 ref 和執行 componentWillUnmount()/effect.destroy()
commitUnmount(node);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
node.child.return = node;
node = node.child;
continue;
}
if (node === root) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
}
複製代碼
解析:
深度優先遍歷執行commitUnmount()
方法
4、commitUnmount()
做用:
同上
源碼:
function commitUnmount(current: Fiber): void {
//執行onCommitFiberUnmount(),查了下是個空 function
onCommitUnmount(current);
switch (current.tag) {
//若是是 FunctionComponent 的話
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
//下面代碼結構和[React源碼解析之Commit第一子階段「before mutation」](https://mp.weixin.qq.com/s/YtgEVlZz1i5Yp87HrGrgRA)中的「3、commitHookEffectList()」類似
//大體思路是循環 effect 鏈,執行每一個 effect 上的 destory()
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const destroy = effect.destroy;
if (destroy !== undefined) {
//安全(try...catch)執行 effect.destroy()
safelyCallDestroy(current, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
break;
}
//若是是 ClassComponent 的話
case ClassComponent: {
//安全卸載 ref
safelyDetachRef(current);
const instance = current.stateNode;
//執行生命週期 API—— componentWillUnmount()
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
//若是是 DOM 標籤的話
case HostComponent: {
//安全卸載 ref
safelyDetachRef(current);
return;
}
//portal 不看
case HostPortal: {
// TODO: this is recursive.
// We are also not using this parent because
// the portal will get pushed immediately.
if (supportsMutation) {
unmountHostComponents(current);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
return;
}
//事件組件 的更新,暫未找到相關資料
case EventComponent: {
if (enableFlareAPI) {
const eventComponentInstance = current.stateNode;
unmountEventComponent(eventComponentInstance);
current.stateNode = null;
}
}
}
}
複製代碼
解析:
主要看三種狀況:
(1) 若是是FunctionComponent
的話,則循環updateQueue
上的effect
鏈,執行每一個effect
上的destory()
方法
safelyCallDestroy()
源碼以下:
//安全(try...catch)執行 effect.destroy()
function safelyCallDestroy(current, destroy) {
if (__DEV__) {
//刪除了 dev 代碼
} else {
try {
destroy();
} catch (error) {
captureCommitPhaseError(current, error);
}
}
}
複製代碼
(2) 若是是ClassComponent
的話
① 執行safelyDetachRef()
,安全卸載ref
safelyDetachRef()
源碼以下:
function safelyDetachRef(current: Fiber) {
const ref = current.ref;
//ref 不爲 null,若是是 function,則 ref(null),不然 ref.current=null
if (ref !== null) {
if (typeof ref === 'function') {
if (__DEV__) {
//刪除了 dev 代碼
} else {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, refError);
}
}
} else {
ref.current = null;
}
}
}
複製代碼
② 執行safelyCallComponentWillUnmount()
,安全調用safelyCallComponentWillUnmount()
safelyCallComponentWillUnmount()
源碼以下:
// Capture errors so they don't interrupt unmounting.
//執行生命週期 API—— componentWillUnmount()
function safelyCallComponentWillUnmount(current, instance) {
if (__DEV__) {
//刪除了 dev 代碼
} else {
try {
//執行生命週期 API—— componentWillUnmount()
callComponentWillUnmountWithTimer(current, instance);
} catch (unmountError) {
captureCommitPhaseError(current, unmountError);
}
}
}
複製代碼
callComponentWillUnmountWithTimer()
源碼以下:
//執行生命週期 API—— componentWillUnmount()
const callComponentWillUnmountWithTimer = function(current, instance) {
startPhaseTimer(current, 'componentWillUnmount');
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
instance.componentWillUnmount();
stopPhaseTimer();
};
複製代碼
本質就是調用componentWillUnmount()
方法,有一點須要注意的是,執行componentWillUnmount()
時,state
和props
都是老state
和props
:
instance.props = current.memoizedProps;
instance.state = current.memoizedState;
instance.componentWillUnmount();
複製代碼
(3) 若是是HostComponent
,也就是 DOM 標籤的話,則執行safelyDetachRef()
,安全卸載 ref
流程圖
GitHubcommitDeletion()
/unmountHostComponents()
/commitNestedUnmounts()
/commitUnmount()
:
github.com/AttackXiaoJ…
(完)