前言
在上篇文章 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…
(完)