Vue组件中watch监听props与v-if动态挂载DOM的冲突及解决方案

作者:组件花园

时间:2026-10-23

浏览:71

本文探讨在Vue组件中,当watch监听props变化并尝试初始化ECharts时,若模板中使用v-if控制DOM挂载,常因DOM未就绪导致getAttribute报错。文章分析了直接监听或mounted中初始化的误区,并提供了将v-if判断移至父组件或使用封装组件的两种有效解决方案,确保数据获取后DOM已挂载再执行渲染逻辑。

文章目录

问题复现与错误分析

问题复现场景:父组件通过名为 source 的 prop 向子组件 Chart 传递数据。


<Chart :source="chartData"></Chart>

import Chart from '../components/Chart'

export default {
 name: 'Home',
 components: { Chart },
 data () {
  return {
   chartData: []
  }
 },
 mounted () {
  setTimeout(() => {
   this.chartData = [
    [89.3, 58212, 'Matcha Latte'],
    [57.1, 78254, 'Milk Tea'],
    [74.4, 41032, 'Cheese Cocoa'],
    [50.1, 12755, 'Cheese Brownie'],
    [89.7, 20145, 'Matcha Cocoa'],
    [68.1, 79146, 'Tea'],
    [19.6, 91852, 'Orange Juice'],
    [10.6, 101852, 'Lemon Juice'],
    [32.7, 20112, 'Walnut Brownie']
   ]
  }, 2000)
 }
}

子组件在接收到 source 数据且该数据存在且至少包含一条记录时,会创建一个 id 为 main 的 div 元素,用于后续初始化 echarts 实例。


<div v-if="source && source.length" id="main" ref="main" style="width: 600px;height: 400px;"></div>
<div vi-else>none</div>

Chart 组件通过监听 prop 的变化,动态调用 echarts 的 setOptions 方法来渲染数据。


export default {
 // ...
 watch: {
  source (newVal, oldVal) {
   this.setOpts()
  }
 },
 props: ['source'],
 methods: {
  setOpts () {
   let myChart = this.$echarts.init(this.$refs.main)
   myChart.setOption({
    dataset: {
     // ...
     source: this.source
    },
    // ...
   })
  }
 }
}

如果按照上述逻辑直接编写代码,必然会导致报错。

错误信息为:Error in callback for watcher "source": "TypeError: Cannot read property 'getAttribute' of undefined"。

在代码中添加一行日志后:


watch: {
  source (newVal, oldVal) {
   console.log(newVal, this.$refs.main) // [Array ...] undefined
   this.setOpts()
  }
 },

可以发现,虽然 source 数据已经存在,但对应的 div 元素尚未挂载到 DOM 中,因此 echarts 无法完成初始化。

于是,很自然地会想到在 mounted 生命周期函数中调用 setOpts 方法:


mounted () {
  console.log(this.source, this.$refs.main) // [] undefined
  this.setOpts()
 },

这样做也是错误的,因为模板语法中使用了 v-if,当 source 不满足条件时,div 元素根本不会挂载,因此依然无法访问到该元素。

错误信息为:Error in mounted hook: "TypeError: Cannot read property 'getAttribute' of undefined"。

解决方案与代码实现

解决这个问题的方法要么去掉 v-if,要么采用另一种写法。

有时我们需要在没有数据的情况下显示一个占位标签,例如展示“暂未获取到数据”等提示信息,此时直接去掉 v-if 显然不可行。

因此,我们保留 v-if 但改变写法:

修改 Chart 组件如下:


<template>
 <div>
  <div id="main" ref="main" style="width: 600px;height: 400px;"></div>
 </div>
</template>

我们只需要一个 source 数据源,在 mounted 时调用 setOpts 方法,当 watch 到数据变化时再次调用以更新数据。


export default {
 name: 'Chart',
 props: ['source'],
 mounted () {
  this.setOpts()
 },
 watch: {
  source () {
   this.setOpts()
  }
 },
 methods: {
  setOpts () {
   let myChart = this.$echarts.init(this.$refs.main)
   myChart.setOption({
    dataset: {
     dimensions: ['score', 'amount', 'product'],
     source: this.source
    },
    xAxis: { type: 'category' },
    yAxis: {},
    series: [
     {
      type: 'bar',
      encode: {
       x: 'product',
       y: 'amount'
      }
     }
    ]
   })
  }
 }
}

我们将 v-if 的判断移到了外部,判断 chartData 是否已获取,一旦获取到数据,立即加载 Chart 组件,从而避开在组件内部调用 v-if 带来的问题:


<template>
 <div>
  <Chart :source="chartData" v-if="flag"></Chart>
  <div v-else>none</div>
 </div>
</template>
import Chart from '../components/Chart'

export default {
 name: 'Home',
 components: { Chart },
 data () {
  return {
   chartData: [],
   flag: false
  }
 },
 methods: {
  getData () {
   setTimeout(() => {
    this.chartData = [
     [89.3, 58212, 'Matcha Latte'],
     [57.1, 78254, 'Milk Tea'],
     [74.4, 41032, 'Cheese Cocoa'],
     [50.1, 12755, 'Cheese Brownie'],
     [89.7, 20145, 'Matcha Cocoa'],
     [68.1, 79146, 'Tea'],
     [19.6, 91852, 'Orange Juice'],
     [10.6, 101852, 'Lemon Juice'],
     [32.7, 20112, 'Walnut Brownie']
    ]
    this.flag = true
   }, 2000)
  }
 },
 mounted () {
  this.getData()
 }
}

此外,还可以将 Chart 组件和占位标签一起封装成一个 ChartWrapper 组件。

这样就不会出现因在组件内部调用 watch 监听 props 变化并动态进行 v-if 判断及挂载数据到 DOM 上所产生的问题。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章