卸载操作
卸载操作发生在更新阶段,更新指的是,在初次挂在完成之后,后续渲染会触发更新,如下面代码所示:
// 初次挂载
renderer.render(vnode,document.querySelector('#app'))
// 再次挂载触发更新
renderer.render(newVnode,document.querySelector('#app'))
更新的情况有好几种,
首先,当后续调用render函数渲染控内容(即null)时,如下面代码所示:
// 初次挂载
renderer.render(vnode,document.querySelector('#app'))
// 新vnode为null,意味着卸载之前渲染的内容
renderer.render(null,document.querySelector('#app'))
在前面我们实现的render函数中,是直接通过innerHTML清空容器,但这样是不严谨的,原因有三点:
- 容器的内容可能是某个或多个组件渲染的,当卸载操作发生时,应该要正确地调用这些组件的beforeUnmount,unmounted等生命周期函数
- 就算内容不是由组件渲染的,有的元素存在自定义指令,应该在卸载操作发生时正确执行对应的指令钩子函数
- 使用innerHTML不会移除绑定在DOM元素上的事件处理函数
正确地卸载方式是:根据vnode对象获取与其相关联的真实DOM元素,然后使用原生DOM操作方法将该DOM元素移除。
所以需要在vnode和真实DOM元素之间建立联系,也就是要修改mountElement函数,代码如下:
function mountElement(vnode, container){
// 让vnode.el引用真实DOM元素
const el = vnode.el = createElement(vnode,type)
if(typeof vnode.children === 'string'){
setElementText(el, vnode.children)
}else if(Array.isArray(vnode.children)){
vnode.children.forEach(child=>{
patch(null,child,el)
})
}
if(vnode.props){
for(const key in vnode.props){
patchProps(el,key,null,vnode.props[key])
}
}
insert(el,container)
}
从上面的代码可以看到,调用createElement函数创建真实DOM元素时,会把真实DOM元素赋值给vnode.el属性,这样就在vnode和真实DOM元素之间建立了联系,因此有了这些只需要根据虚拟节点对象vnode.el取得真实DOM元素,再将其从父元素中移除即可,看下面代码:
function render(vnode,container){
if(vnode){
patch(container._vnode,vnode,container)
}else{
if(container._vnode){
// 根据vnode获取要卸载的真实DOM元素
const el = container._vnode.el
// 获取el的父元素
const parent = el.parentNode
// 调用removeChild
if(parent) parent.removeChild(el)
}
}
container._vnode = vnode
}
由于卸载操作比较常见且基础,所以将其封装到unmount函数中,后续可以复用,代码如下:
function unmount(vnode){
const parent = vnode.el.parentNode
if(parent){
parent.removeChild(vnode.el)
}
}
有了unmount函数后,就可以在render函数中完成卸载工作了
function render(vnode,container){
if(vnode){
patch(container._vnode,vnode,container)
}else{
if(container._vnode){
unmount(container._vnode)
}
}
container._vnode = vnode
}
区分vnode的类型
当后续调用render函数渲染空内容(即null)时,会执行卸载操作。如果后续渲染时,为render函数传递了新的vnode,则不会进行卸载操作,而是把新旧vnode都传递给patch函数进行打补丁操作,但是在执行打补丁操作之前,需要保证新旧vnode所描述的内容相同,就比如说,初次渲染的vnode是一个p元素,后续又渲染了一个input元素,这就会造成新旧vnode描述的内容不同,即vnode.type属性的值不同。这样打补丁是没有意义的,因为对于不同的元素来说,每个元素都有特有的属性。
在这种情况下,要先将p元素卸载,再将input元素挂载到容器中。因此需要调整patch函数的代码:
function patch(n1,n2,container){
// 如果n1存在,则对比n1和n2的类型
if(n1 && n1.type !== n2.type){
// 如果新旧vnode的类型不同,则直接将旧的vnode卸载
unmount(n1)
// 卸载完后,要将n1的值重置为null,保证后续挂载操作正确执行
n1 = null
}
if(!n1){
mountElement(n2,container)
}else{
// 更新操作
}
}
但是新旧vnode仅仅是描述的内容相同是不够的,还要确认类型是否相同,要根据不同的类型,提供不同的挂载或打补丁的处理方式,所以要继续完善patch函数,代码如下:
function patch(n1,n2,container){
// 如果n1存在,则对比n1和n2的类型
if(n1 && n1.type !== n2.type){
// 如果新旧vnode的类型不同,则直接将旧的vnode卸载
unmount(n1)
// 卸载完后,要将n1的值重置为null,保证后续挂载操作正确执行
n1 = null
}
const {type} = n2
// 如果n2.type的类型是字符串,则描述的是普通标签元素
if(typeof type === 'string'){
if(!n1){
mountElement(n2,container)
}else{
// 更新操作
}
}else if(typeof type === 'object'){
// 如果类型是对象则描述的是组件
}else if(typeof type === 'xxx'){
// 其他类型的vnode
}
}
根据vnode.type类型的不同,做相应的处理