组件mounted后通过document.body.appendChild挂载到body下的问题

您所在的位置:网站首页 vue怎么让组件重新加载 组件mounted后通过document.body.appendChild挂载到body下的问题

组件mounted后通过document.body.appendChild挂载到body下的问题

#组件mounted后通过document.body.appendChild挂载到body下的问题| 来源: 网络整理| 查看: 265

此篇文章主要目的是记录一下这个问题,因为涉及到vue更新过程,所以如果不了解vue更新过程的话,还是挺难找到原因的。

这是一个很简单的弹窗组件,通过默认slot来插入弹窗的主体内容,value属性控制弹窗显示还是隐藏,通过v-model和父组件通信

{{ title }} export default { props: { value: { type: Boolean, default: false, }, title: { type: String, default: '', }, closeable: { default: true, }, }, mounted() { document.body.appendChild(this.$el); }, beforeDestroy() { document.body.removeChild(this.$el); }, }; 复制代码

先解释一下为什么mounted后要挂载到body下,因为弹窗组件的遮罩层是fixed定位,这个fixed定位往往希望参照的父元素是body,这样可以让遮罩铺满这个屏幕。但是fixed定位参照的父元素不一定是body,详细见mdn说明,所以弹窗组件一般都选择挂载到body下。

最开始的时候,这个组件我并没有定义beforeDestroy钩子,因为这里只是移动了dom元素,对于vue来说,vnode的结构并没有改变,并且vnode上的elm属性保持着对这个dom元素的引用,对于patch来说不应该会有问题。事实证明,父元素切换传进来的value值的时候,弹窗组件也是能正常显示隐藏。

但是,当这个弹窗用在beforeRouterLeave的拦截弹窗时,bug就来了。描述下具体现象就是触发beforeRouterLeave,开启弹窗,弹窗里面选择cancel就不离开当前页面,也就是next(false),选择confirm,也就是next(),正常离开页面。如果点cancel,弹窗是可以关闭的。但是如果点来comfrim这时候页面可以正常离开,但是弹窗依然在,控制弹窗显示的属性值已经为false。后面无论是点cancel和comfirm,都无法关闭弹窗。

思路分析:

显然这个bug和路由强相关,首先我回顾了下路由切换做了什么,路由切换的时候会重新匹配新的组件,改变this.route属性值,this.route属性值,this.route属性值,this.route是响应式的,router-view的父组件会收集到相关依赖*(这里的父组件指的是具有$parent关系的,而不是页面层级上的父组件,比如keep-alive包裹的router-view,但是keep-alive不是router-view的父组件)*,最终会触发router-view组件重新render拿到新的vnode,父组件patch时候,旧组件的整颗dom树会被移除。

到这里其实已经有点复杂了,所以我把问题简化一下,我用下面三个组件模拟上面的场景

app组件下面有a,b两个组件 ,初始化显示a组件,2s后切换为b组件

//app.vue created() { setTimeout(() => { this.showA = false }, 2000) }, 复制代码

a组件里面同样有个这种操作

a组件 mounted() { document.body.appendChild(this.$refs.b) }, 复制代码

2s后,可以发现p标签没了,body下面的b标签还在。

image.png

这里例子简单很多,在vue源码的patch方法打个断点走一遍就能发现,移除的dom实际上已经不包含b标签这块的元素了,所以b标签还在页面上。到这里就解释了为什么弹窗会一直在页面上。

但是弹窗点cancel的时候为什么又会消失,其实上面有提到,这是因为弹窗组件自身的patch从而销毁。

稍微复杂的问题来了,那么点comfirm的时候一样会改变value的值,导致弹窗组件patch,为什么这里就不能生效呢?

这是因为组件的patch是异步的,延迟了一个tick,在一个延迟的tick里面,beforeRouterLeave钩子已经走完了,外面的父组件已经开始patch新旧两个router-view里面的组件了,那么旧组件全部都会走一遍destroy方法,在destroy方法,在destroy方法,在destroy方法里面,会对所有子组件的watcher进行teardown。注意看active已经被置为false

Watcher.prototype.teardown = function teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this); } var i = this.deps.length; while (i--) { this.deps[i].removeSub(this); } this.active = false; } }; 复制代码

等一个tick后,弹窗组件的watcher应该要run了, 被这个actice标记拦截了。所以弹窗组件不会走到patch。

Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value var oldValue = this.value; this.value = value; if (this.user) { var info = "callback for watcher \"" + (this.expression) + "\""; invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info); } else { this.cb.call(this.vm, value, oldValue); } } } }; 复制代码

如果过destroy的teardown逻辑注释调,这个弹窗在路由离开后也是可以正常销毁的。

解决方法很简单,在beforeDestroy钩子中手动移除就好。对于这个问题,总结起来就是为了避免我们考虑不到的场景,如果移动了dom,一定要在组件销毁前手动移除掉。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3