8.5 卸载操作
初次挂载完成后,后续渲染若传入
null作为新 vnode,就意味着什么都不渲染,此时需要卸载之前渲染的内容 —— 但不能简单粗暴地清空容器。
一、卸载的入口
renderer.render(null, document.querySelector('#app'))
render 中的分支:新 vnode 不存在、旧 vnode 存在,就是卸载:
function render(vnode, container) {
if (vnode) {
// 新 vnode 存在 → 打补丁
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
// 旧 vnode 存在,新 vnode 不存在 → 卸载
unmount(container._vnode)
}
}
container._vnode = vnode
}
二、为什么不能用 innerHTML 清空
container.innerHTML = '' 看似一行搞定,但不严谨,原因有三点:
| 缺陷 | 后果 |
|---|---|
| 内容可能是由一个或多个组件渲染的 | 卸载时应正确调用组件的 beforeUnmount、unmounted 等生命周期函数,innerHTML 不会触发 |
| 元素上可能有自定义指令 | 卸载时应正确执行对应的指令钩子函数,innerHTML 不会触发 |
| 不移除事件 | 不会移除绑定在 DOM 元素上的事件处理函数,留下隐患 |
三、正确的卸载方式:vnode.el
关键在于让 vnode 与真实 DOM 元素之间建立联系:挂载时顺手保存引用。
function mountElement(vnode, container) {
// 让 vnode.el 引用真实 DOM 元素
const el = vnode.el = createElement(vnode.type)
// ...处理 children / props
insert(el, container)
}
卸载时按 vnode.el 取到真实 DOM,用原生 DOM 操作把它从父元素中移除:
function unmount(vnode) {
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
四、为什么要封装成 unmount
卸载是常见且基本的操作,封装后便于复用,还额外带来两点好处:
- 在
unmount内部,我们有机会调用绑定在 DOM 元素上的指令钩子函数(如beforeUnmount、unmounted); - 当
unmount执行时,我们有机会检测 vnode 的类型:若描述的是组件,可以调用组件相关的生命周期函数;若是 Fragment,则要卸载它的 children(见 8.11)。
后续凡是删除节点的地方(8.6 类型不同时的整体替换、8.9 更新子节点等),都复用
unmount,不要各写一份removeChild。
相关
- 8.4 class的处理:挂载阶段的属性处理
- 8.6 区分vnode类型:类型不同时先用
unmount卸载旧节点