前言
在上篇文章 React源码解析之Commit第二子阶段「mutation」(中) 中,我们讲了 「mutation
」 子阶段的更新(Update
)操作,接下来我们讲删除(Deletion
)操作:
case Deletion: {
//删除节点
commitDeletion(nextEffect);
break;
}
一、commitDeletion()
作用:删除 DOM 节点
源码:
代码语言:javascript复制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()
(2) 执行detachFiber()
,重置fiber
属性
detachFiber()
的源码如下:
//重置 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()
二、unmountHostComponents()
作用:
删除目标节点及其子节点,如果目标节点或子节点是类组件ClassComponent
的话,会执行内部的生命周期 API——componentWillUnmount()
源码:
代码语言:javascript复制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」(上) 中的 「 二、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,请参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/removeChild
(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()
源码
三、commitNestedUnmounts()
作用: 深度优先遍历,循环执行: 在目标节点被删除前,从该节点开始深度优先遍历,卸载该节点及其子节点 ref 和执行该节点及其子节点 componentWillUnmount()/effect.destroy()
源码:
代码语言:javascript复制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()
方法
四、commitUnmount()
作用: 同上
源码:
代码语言:javascript复制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)中的「三、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
流程图
GitHub
commitDeletion()
/unmountHostComponents()
/commitNestedUnmounts()
/commitUnmount()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js
(完)