本文介绍
- 前面我们讲述了组件的挂载、更新等流程。本文继续讲解组件挂载流程。挂载流程主要由
unmount
、unmountComponent
、unmountChildren
三个函数控制。下面主要以unmountComponent
为起点剖析组件的卸载流程。
卸载流程
1.卸载组件(unmountComponent)
const unmountComponent = (instance, parentSuspense, doRemove) => {
const { bum, update, subTree, um } = instance;
if (bum) {
shared.invokeArrayFns(bum);
}
if (update) {
update.active = false;
unmount(subTree, instance, parentSuspense, doRemove);
}
if (um) {
queuePostRenderEffect(um, parentSuspense);
}
queuePostRenderEffect(() => {
instance.isUnmounted = true;
}, parentSuspense);
};
- 立即调用当前组件所有的
beforeUnmount钩子
。
- 让组件实例的更新任务立即失活,即使当前更新任务还在调度器队列当中也不会执行。
- 卸载组件的子节点。
- 将
unmount钩子函数
放入调度器后置队列中,后置队列没有优先级,先放入先执行。当时子组件的卸载在父组件卸载之前,所以子组件的unmount钩子函数
会先放入调度器后置队列当中,保证执行钩子的执行顺序不会错误。
- 在完成卸载后标识当前组件已经被卸载。
2.unmount
const unmount = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs,
} = vnode;
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true);
}
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs;
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component, parentSuspense, doRemove);
} else {
}
};
- 在
vnode
中获取必要的属性。
- 将所有的
ref
设置为null
。setRef
在Vue3源码分析(4)已经详细讲解过了。
- 根据
shapeFlag
判断当前节点是否是组件,如果是组件则卸载组件。
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, "beforeUnmount");
}
else if (
dynamicChildren &&
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
) {
unmountChildren(
dynamicChildren,
parentComponent,
parentSuspense,
false,
true
);
}
else if (
(type === Fragment &&
patchFlag &
(PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
(!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
) {
unmountChildren(children, parentComponent, parentSuspense);
}
if (doRemove) {
remove(vnode);
}
- 处理当前节点不是组件的情况。
- 调用当前节点自定义指令的
beforeUnmount钩子
。
STABLE_FRAGMENT
:稳定的Fragment节点
,我们知道在Vue2
的template
中需要返回一个根节点,但是Vue3
则不需要返回一个根节点,这是因为Vue3
会自动给组件包裹一层Fragment类型节点
。
<template>
<div>
<p></p>
<p></p>
</div>
</template>
<template>
<p></p>
<p></p>
</template>
- 对于
Vue3写法
的节点就需要包裹一层稳定的Fragment
。
UNKEYED_FRAGMENT、KEYED_FRAGMENT
:不稳定的Fragment
。首先只要使用了v-for
指令那么就会包裹一层Fragment类型节点
,当我们写出<div v-for="a in b"></div>
这样的代码,这就是UNKEYED_FRAGMENT
类型,当我们写出<div v-for="a in b" :key="a"></div>
这样的代码,这就是KEYED_FRAGMENT
类型,他们都是不稳定的Fragment类型节点
。
- 这里需要详细讲解一下为什么
稳定的Fragment类型节点
是卸载dynamicChildren
,而其他的Fragment类型节点
需要卸载所有的children
。对于不稳定的Fragment节点
,无法收集dynamicHildren属性
,这是因为使用了v-for
渲染列表之后,新旧节点在数组中的顺序可能发生了改变违背了dynamicChilren
一一比较的原则,所以对于使用v-for
渲染的Fragment类型节点
禁止收集dynamicChilren
。那么对于稳定的Fragment节点
则允许收集所以可以直接卸载dynamicChildren
即可。
<template>
<div v-for="a in b" :key="a"></div>
<div></div>
</template>
const _hoisted_1 = _createElementVNode("div")
function render(ctx, cache) {
return (openBlock(), createElementBlock(Fragment, null, [
(openBlock(true), createElementBlock(Fragment, null, renderList(ctx.b, (a) => {
return (openBlock(), createElementBlock("div", { key: a }))
}), 128 )),
hoisted_1
], 64 ))
- 我们可以发现
STABLE_FRAGMENT
的openBlock
没有传递true
参数表示允许收集dynamicChildren
,对于KEYED_FRAGMENT
的openBlock
则传递了true
参数表示不允许收集dynamicChildren。具体大家也可以看看Vue3源码分析(10)详细讲解了如何收集动态节点。
if (shouldInvokeDirs) {
queuePostRenderEffect(() => {
invokeDirectiveHook(vnode, null, parentComponent, "unmounted");
}, parentSuspense);
}
- 最后把节点自定义指令的
unmounted
钩子函数放入调度器后置队列中等待执行。
3.卸载DOM(remove)
const remove = (vnode) => {
const { type, el, anchor } = vnode;
if (type === Fragment) {
removeFragment(el, anchor);
return;
}
if (type === Static) {
removeStaticNode(vnode);
return;
}
hostRemove(el);
};
- 判断当前需要移除节点的类型,调用不同的移除DOM函数处理。
const hostRemove = function(){
const parent = child.parentNode;
if (parent) parent.removeChild(child);
}
const removeFragment = (cur, end) => {
let next;
while (cur !== end) {
next = hostNextSibling(cur);
hostRemove(cur);
cur = next;
}
hostRemove(end);
};
hostRemove
由react-dom
实现,实际就是调用浏览器的Api实现对DOM的移除。
const removeStaticNode = ({ el, anchor }) => {
let next;
while (el && el !== anchor) {
next = hostNextSibling(el);
hostRemove(el);
el = next;
}
hostRemove(anchor);
};
4.unmountChildren
const unmountChildren = (
children,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
start = 0
) => {
for (let i = start; i < children.length; i++) {
unmount(
children[i],
parentComponent,
parentSuspense,
doRemove,
optimized
);
}
};
总结
- 本章主要分析了组件的卸载流程。到此为止,组件的挂载、更新、卸载流程已经全部分析完毕。下面我们还会详细讲解内置组件
Teleport、Suspense
的实现原理。