View Design的Modal组件transfer属性详解(Modal与v-if/v-for搭配使用时的陷阱)

1,478 阅读3分钟

问题复现

首先我们来看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>

image.png

运行上面的代码然后点击「显示弹框」按钮,弹框可以被正常显示,关闭弹框后然后再点击「移除弹框」将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,就不存在移来移去的问题了