【前端框架】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

可能还有更简洁、高效的方式,知道的朋友可以告知一下,相互学习。

  • 13
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值