【前端框架】Element UI Dialog 组件中执行 DOM 操作异常问题的分析与处理
文章目录
1 问题描述
使用 Element UI dialog 组件,在 dialog 对话框中渲染图表,需要获取图表挂载点的 dom 元素。由于 Element UI 的 dialog_body 是以 lazy 模式进行渲染,导致 dialog 打开时,图表加载失败!
示例页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
<div id="app">
<el-button @click="visible = true">Button</el-button>
<el-dialog :visible.sync="visible" @open="handleOpen" title="Hello world">
<div id="hello"><p>Try Element</p></div>
</el-dialog>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: function() {
return { visible: false }
},
methods: {
handleOpen(){
console.log("open: "+$("#hello p").html()); // TAG 1
}
}
})
</script>
</html>
element ui dialog 组件提供了 open 事件,在 Dialog 打开时执行回调。
官网对 open 的解释如下
Dialog 的内容是懒渲染的,即在第一次被打开之前,传入的默认 slot 不会被渲染到 DOM 上。因此,如果需要执行 DOM 操作,或通过 ref 获取相应组件,请在 open 事件回调中进行。
可是实际使用时,发现并非如此。
在上例 TAG 1 所处的时间点,实际页面如下:
<div id="app">
<button type="button" class="el-button l-button--default">
<span>Button</span>
</button>
<div class="el-dialog__wrapper" style="display: none;">
<div class="el-dialog" style="margin-top: 15vh;">
<div class="el-dialog__header">
<span class="el-dialog__title">Hello world</span>
<button type="button" aria-label="Close" class="el-dialog__headerbtn"><i class="el-dialog__close el-icon el-icon-close"></i></button>
</div>
<!---->
<div class="el-dialog__footer"><div>Hello Hello</div></div>
</div>
</div>
</div>
可见,这个时候的页面并不完整,是无法通过选择器获取 dom 元素的,那么对 “#hello” dom 元素的所有操作都是无效的。
此时检测到 dialog 组件的状态如下:
vm.visible // true
vm.$children[1].visible // true
vm.$children[1].rendered // true
vm.$children[1].opened // false
2 问题分析
el-dialog 组件会监听 visible 的状态,当状态为 true 时,立刻触发 open 事件,但这个时候 el-dialog__body 的内容还没有渲染。因为 Vue 组件通过 $emit 触发的事件并不是异步执行的,而是同步执行。
代码如下:
watch: {
visible: function(e) {
var t = this;
e ? (this.closed = !1,
this.$emit("open"),
this.$el.addEventListener("scroll", this.updatePopper),
this.$nextTick(function() {
t.$refs.dialog.scrollTop = 0
}),
this.appendToBody && document.body.appendChild(this.$el)) : (this.$el.removeEventListener("scroll", this.updatePopper),
this.closed || this.$emit("close"))
}
}
正如官方文档所说,Dialog 的内容,也就是 el-dialog__body 中的内容是懒加载的!
那么它懒加载的机制是什么呢?
查看 element dialog 组件的代码,会发现:
<div class="el-dialog__body" v-if="rendered"><slot></slot></div>
Vue 中 v-if 指令的特征是,根据表达式的值在 DOM 中生成或移除一个元素,如果 rendered 为 false,那么该 dom 元素会被移除,当 rendered 为 true 时,对应元素的克隆会被重新插入到 DOM 中。 而 v-show 指令则是根据表达式的值来显示或隐藏 HTML 元素,并不会移除 DOM 元素。
v-if 是惰性的,如果初始渲染时条件为假,则什么也不做,在条件第一次变为真时才开始局部编译(编译结果会缓存起来)。
这就是 element dialog-body 懒加载的原理。
3 解决方案
3.1 将元素放在 footer slot 中
dialog 组件中的 footer slot 是实时渲染的,放在其中的 dom 元素可以直接获取:
<div id="app">
<el-button @click="visible = true">Button</el-button>
<el-dialog :visible.sync="visible" @open="handleOpen" title="Hello world">
<div slot="footer"><div id="hello"><p>Try Element</p></div></div>
</el-dialog>
</div>
3.2 在 open 事件中延迟执行具体的代码
由于浏览器是单线程执行前端代码,因此可以短暂的交出控制权,让其进行渲染页面,然后再执行具体的任务。
var vm = new Vue({
el: '#app',
data: function() {
return { visible: false }
},
methods: {
handleOpen(){
setTimeout(console.log, 100, "open: "+$("#hello p").html()) // TAG 1
}
}
})
3.3 在 update 回调函数执行相关的逻辑
当 v-if 为 true 时,会重新渲染页面,渲染结束后会触发 update 回调函数,这个时候可以用来执行一些代码。
var vm = new Vue({
el: '#app',
data: function() {
return {
visible: false,
see: true,
nosee: false
}
},
updated: function(){
var dialog = this.$children[1]
if(dialog.rendered && this.visible) {
console.log("update: " + $("#hello p").html())
}
}
})
需要注意的时,要对 dialog 的状态进行验证,确保代码的执行时机准确无误。
3.4 主动更改 rendered 的值为 true
在 open dialog 之前,先将 rendered 的值改为 true
var vm = new Vue({
el: '#app',
data: function() {
return { visible: false }
},
methods: {
handleOpen(){
console.log("open: "+$("#hello p").html()); // TAG 1
}
}
})
vm.$children[1].rendered = true
可能还有更简洁、高效的方式,知道的朋友可以告知一下,相互学习。
更多推荐
所有评论(0)