问题复现
首先我们来看view design官网对Modal组件transfer属性的解释:是否将弹层放置于 body 内。意思就是transfer为true时,Modal组件默认会放到body内,否则会放到组件对应的节点中。默认情况下transfer为true。
正常情况下我们都会使用Modal的v-model
属性来控制Modal是否展示,这个过程Modal会一直存在于Dom中,但是有些情况我们可能会希望将Modal从Dom中移除,于是有了下面的代码(可以再codesandbox中调试: codesandbox example):
<template>
<div>
<div class="margin: 10px">
{{ displayModal ? "将Modal从dom中移除:" : "将Modal添加到dom中:" }}
<Button type="primary" @click="toggleModal">
{{ displayModal ? "移除Modal" : "添加Modal" }}
</Button>
</div>
<div v-if="displayModal" style="margin: 10px">
通过v-model控制弹框显示:
<Button type="primary" @click="modal1 = true">显示弹框</Button>
</div>
<div>
<Modal
v-if="displayModal"
v-model="modal1"
title="弹框测试"
@on-ok="ok"
@on-cancel="cancel"
>
<p>Content of dialog</p>
<p>Content of dialog</p>
<p>Content of dialog</p>
</Modal>
</div>
</div>
</template>
<script>
export default {
data() {
return {
displayModal: true,
modal1: false,
};
},
methods: {
ok() {
this.$Message.info("Clicked ok");
},
cancel() {
this.$Message.info("Clicked cancel");
},
toggleModal() {
this.displayModal = !this.displayModal;
},
},
};
</script>
运行上面的代码然后点击「显示弹框」按钮,弹框可以被正常显示,关闭弹框后然后再点击「移除弹框」将Modal从dom中移除掉,然后再点击「添加弹框」将Modal添加回Dom,这时在点击「显示弹框」会发现弹框无法显示了。嗯?弹框被添加回Dom,应该不影响显示才对啊,怎么回事?
先讲解决方法,将Modal中的”v-if="displayModal"“移到包裹他的那个div 或者 将 Modal的transfer属性设置为false 你会发现一切都正常了
<div v-if="displayModal">
<Modal
v-model="modal1"
title="弹框测试"
@on-ok="ok"
@on-cancel="cancel"
>
<p>Content of dialog</p>
<p>Content of dialog</p>
<p>Content of dialog</p>
</Modal>
</div>
看到这里估计大家都能大致猜出问题出现的原因是 默认情况下(transfer为true时)Modal组件被放置到的body中,当displayModal为false时vue去移除Modal发现该节点不存在,然后就出问题了。但是view-design既然把Modal放到了body中,那移除元素前肯定会移回来才对,为了搞清楚具体细节,我们直接干源码,心急的小伙伴可以直接看结论。
源码分析
通过阅读modal源码 我们可以发现modal组件最外层的div使用了一个自定义指令v-transfer-dom
(不熟悉自定义组件的小伙伴看这里),继续阅读 transfer-dom指令源码 我们可以知道当Modal被插入到dom时会执行inserted
函数,inserted
会将Modal移到body中并且记录移动前的父节点,当Modal移除后会执行unbind(重点在这里),unbind会检查父节点是否还存在,如果还存在的话就将Modal添加回父节点。
当"v-if="displayModal""写在Modal组件上时,Modal的父节点为包裹它的div,这个div并不会被销毁,所以在Modal移除后,unbind又将它添加回这个div,导致Modal没有被正确移除掉
当"v-if="displayModal""写在包裹Modal的div上时,移除的是Modal父节点,unbind发现父节点不存在,就自然就不会再次添加了
而将transfer设置为true,不存在Modal移来移去的情况,当然就不会有问题
...
inserted (el, { value }, vnode) {
if ( el.dataset && el.dataset.transfer !== 'true') return false;
el.className = el.className ? el.className + ' v-transfer-dom' : 'v-transfer-dom';
const parentNode = el.parentNode;
if (!parentNode) return;
const home = document.createComment('');
let hasMovedOut = false;
if (value !== false) {
parentNode.replaceChild(home, el); // moving out, el is no longer in the document
getTarget(value).appendChild(el); // moving into new place
hasMovedOut = true
}
if (!el.__transferDomData) {
el.__transferDomData = {
parentNode: parentNode,
home: home,
target: getTarget(value),
hasMovedOut: hasMovedOut
}
}
},
...
unbind (el) {
if (el.dataset && el.dataset.transfer !== 'true') return false;
el.className = el.className.replace('v-transfer-dom', '');
const ref$1 = el.__transferDomData;
if (!ref$1) return;
if (el.__transferDomData.hasMovedOut === true) {
el.__transferDomData.parentNode && el.__transferDomData.parentNode.appendChild(el)
}
el.__transferDomData = null
}
...
结论
transfer属性为true时,会触发Modal组件的transfer-dom指令,这个指令会在Modal插入dom时获取Modal的父节点并将Modal移动到body下,当Modal被移除后这个指令又会去判断之前获取的父节点还在不在,还在的话就会将组件重新添加回去。
为了避免这种情况,Modal尽量不要跟v-if或者v-for(会导致Modal销毁和添加)一起使用,如果一定要用请放到Modal的父div上。当然最好的办法还是将transfer设置为false,就不存在移来移去的问题了