写点什么

Echarts 与 Vue3 中获取 DOM 节点可能出现的异常错误

作者:秃头小帅oi
  • 2025-03-05
    福建
  • 本文字数:4900 字

    阅读完需:约 16 分钟

Echarts与Vue3中获取DOM节点可能出现的异常错误

官方:返回一个浅层 ref,其值将与模板中的具有匹配 ref attribute 的元素或组件同步。参数匹配机制‌:useTemplateRe 的参数需与模板中 ref 属性值必须完全一致‌响应式变量类型明确‌:返回值是一个 浅层 ref 对象,其 .value 直接指向绑定的 DOM 元素或组件实例。

useTemplateRef 源码浅析

packages/runtime-core/src/helpers/useTemplateRef.ts 文件中

import { type ShallowRef, readonly, shallowRef } from '@vue/reactivity'import { getCurrentInstance } from '../component'import { warn } from '../warning'import { EMPTY_OBJ } from '@vue/shared'export function useTemplateRef(key) {  // 获取当前 Vue 实例对象。  const i = getCurrentInstance()  // 创建一个浅层的ref对象r,初始值为null。   const r = shallowRef(null)  //如果存在 Vue 实例  if (i) {    // i.refs 默认初始化为 EMPTY_OBJ(空对象)首次使用时动态创建新对象‌    const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs    Object.defineProperty(refs, key, {        enumerable: true,        get: () => r.value,        set: val => (r.value = val),      })  }  return r}
复制代码

1.获取当前 Vue 实例对象。2.创建一个浅层的 ref 对象 r,初始值为 null。3.如果存在 Vue 实例,则获取实例上的 refs 属性(用于存储模板引用)。4.使用 Object.defineProperty 对 refs 对象的 key 属性进行拦截:get 拦截:返回 r.value,即 useTemplateRef 返回的 ref 变量的值。set 拦截:将 val 赋值给 r.value,从而将 DOM 元素或组件实例绑定到 ref 变量上。5.返回 ref 对象 r。

下面这段代码看看有啥问题

<template>  <div>    <div class="pic" ref="chartNode"></div>  </div></template><script setup>import * as echarts from 'echarts'import { onMounted, useTemplateRef } from 'vue'// useTemplateRef接受的是一个字符串,表示你要获取哪一个节点const divNode = useTemplateRef("chartNode");console.log(1111, divNode)// 初始化图表let chartDom = echarts.init(divNode);onMounted(() => {  drawCharts()})const drawCharts=()=>{  // 指定图表的配置项和数据  let option = {    xAxis: {      type: 'category',      data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月']    },    yAxis: {      type: 'value'    },    tooltip: {      trigger:'axis',    },    series:[      {        data: [1150, 200, 300, 356, 105, 200, 345],        type: 'line'      }    ]  };  // 使用刚指定的配置项和数据显示图表。  chartDom.setOption(option);}</script><style scoped>.pic{  width: 600px;  height: 380px;}</style>
复制代码



实际情况:this.dom.getContext is not a function

报错信息: Uncaught (in promise) TypeError: this.dom.getContext is not a function01-echarts 引入报错.jpg 为啥会报这个错误呢?原因:因为在初始化 echarts 的时候,echarts 规定只能传入实际的 DOM 元素。此时我们传递的是 Ref 对象,而不是实际的 DOM 元素需要传递 divNode.value。

传递一个实际的 DOM 元素(divNode.value)

const divNode = useTemplateRef("chartNode");console.log(1111, divNode)// 初始化图表,传递实际DOMlet chartDom = echarts.init(divNode.value);onMounted(() => {  drawCharts()})const drawCharts=()=>{  ...代码爆出不变}
复制代码



为啥还会报错:Error: Initialize failed: invalid dom.

原因在于:let chartDom = echarts.init(divNode.value);这一行代码。此时 divNode.value 为 null。为啥是 null 呢?这个跟获取时机有关:此时还没有完成绑定哈,所以得到的是 null。什么时候可以正常绑定呢?第 1 种:在 onMounted 中肯定是绑定了,此时 divNode.value 是一个实际的 DOM 元素了。第 2 种:与它同一级的 DOM 元素已经渲染完成(其实也是在 onMounted)

使用 useTemplateRef 正常渲染图表

<template>  <div>    <div class="pic" ref="chartNode"></div>  </div></template>
<script setup>import * as echarts from 'echarts'import { onMounted, useTemplateRef } from 'vue'let chartDom = null //存储的是 ECharts 实例// useTemplateRef接受的是一个字符串,表示你要获取哪一个节点const divNode = useTemplateRef("chartNode");console.log(222, divNode)onMounted(() => { // 在onMounted中初始化图表,可以得到原生的DOM节点 chartDom = echarts.init(divNode.value); // 调用图表 drawCharts()})const drawCharts=()=>{ // 指定图表的配置项和数据 let option = { xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'] }, yAxis: { type: 'value' }, tooltip: { trigger:'axis', }, series:[ { data: [1150, 200, 300, 356, 105, 200, 345], type: 'line' } ] }; // 使用刚指定的配置项和数据显示图表。 chartDom.setOption(option);}</script><style scoped>.pic{ width: 600px; height: 380px;}</style>
复制代码



DOM 元素已经渲染完成,useTemplateRef 拿到 DOM 元素

<template>  <div class="art-page">    <div ref="divNode">我是div元素</div>    <button @click="getNodeHandler">我是按钮,获取div节点</button>  </div></template><script setup>import { useTemplateRef } from 'vue'const divNode = useTemplateRef("divNode");const getNodeHandler= () =>{  /**   * 为啥这里的 divNode.value不是null   * 因为:与它同一级的DOM元素已经渲染完成。   * 也就是说:点击的时候已经渲染完成了。也就完成了DOM的绑定,因此可以拿到DOM元素  */  if(divNode.value){    divNode.value.innerText = '通过dom来赋值';  }}</script>
复制代码



当然我们除了使用 useTemplateRef 来获取 DOM 元素还可以使用 ref 来获取 DOM 元素

使用 ref 来获取 DOM 元素

使用 ref 来获取 DOM 元素需要注意的点。通过 ref 函数创建,并赋值给与模板中同名的变量。

<div class="pic" ref="chartNode"></div> <script setup>// 通过ref函数创建,并赋值给与模板中同名的变量。const chartNode = ref(null)</script>
复制代码

错误的获取 DOM 节点的方式

<div class="pic" ref="chartNode"></div> <script setup>// 这一种是创建了一个响应式的对象。并不是获取DOM节点。const node = ref('chartNode')</script>
复制代码

使用 ref 来获取 DOM 元素,并渲染 echarts

<template>  <div>    <div class="pic" ref="chartNode"></div>  </div></template>
<script setup>import * as echarts from 'echarts'import { onMounted,ref } from 'vue'let chartDom = null //存储的是 ECharts 实例let chartNode = ref() // 通过ref函数创建,并赋值给与模板中同名的变量。onMounted(() => { // 在onMounted中初始化图表,可以得到原生的DOM节点 chartDom = echarts.init(chartNode.value); // 调用图表 drawCharts()})const drawCharts=()=>{ // 指定图表的配置项和数据 let option = { xAxis: { type: 'category', data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'] }, yAxis: { type: 'value' }, tooltip: { trigger:'axis', }, series:[ { data: [1150, 200, 300, 356, 105, 200, 345], type: 'line' } ] }; // 使用刚指定的配置项和数据显示图表。 chartDom.setOption(option);}</script><style scoped>.pic{ width: 600px; height: 380px;}</style>
复制代码

存储的是 ECharts 实例变成响应式数据出现的问题

<template>  <div class="chart-page">    <div class="pic" ref="chartNode"></div>  </div></template><script setup>import * as echarts from 'echarts'import { onMounted,ref, onBeforeUnmount  } from 'vue'// 存储 echarts 实例的变量et chartDom = ref(null)let chartNode  = ref()onMounted(() => {  // 存储的是 ECharts 实例此时是一个响应式的  chartDom.value = echarts.init(chartNode.value);  // 调用图表  drawCharts()  // 监听窗口大小变化,重绘图表  window.addEventListener('resize', chartRedraw)})onBeforeUnmount(()=>{  window.removeEventListener('resize', chartRedraw)})const drawCharts = () => {  let option = {    ...配置不变  }  chartDom.value.setOption(option);}const chartRedraw = ()=> {  chartDom.value && chartDom.value.resize()}</script>
复制代码



缩放 echarts 出现:Cannot read properties of undefined (reading 'type')

缩放窗口大小,echarts 图表出现报错信息如下:Cannot read properties of undefined (reading 'type')原因是:存储的是 ECharts 实例变成了响应式,从而在 resize 的时候获取不到。其实存储 ECharts 实例不应该是一个响应式的数据。就是一个普通类型的数据就行

解决办法

现在我们知道出现问题的原因。解决办法就是不让它变成响应式的数据就行。1:存储的是 ECharts 实例不要变成响应式,让其成为普通对象。2.使用 markRaw 将它标记为一个对象,使其不会被转换为响应式对象第 1 种我们已经使用过了,下面我们演示第 2 种

Vue3 中的 markRaw

我的理解是: ****用于‌标记一个对象,使其永远不会被转换为响应式对象‌。返回该对象本身。该对象即使被包裹在 reactive()、ref()、shallowReactive() 等响应式 API 中也会保持原始状态,不会触发依赖追踪和视图更新‌。官网的解释是:将一个对象标记为不可以被转化为代理对象。返回该对象本身。

使用 markRaw 来解决

<script setup>import { onMounted,ref, onBeforeUnmount, markRaw } from 'vue'// ...省略其他代码,其他代码不变,使用markRaw包裹chartDom.value = markRaw(echarts.init(chartNode.value));// ...省略其他代码,其他代码不变</script>
复制代码

切换页面的时候 echarts 实例会自动销毁嘛?

不会的。需要手动销毁。‌在 Vue 中切换路由(页面)时或刷新页面时,ECharts 实例不会自动销毁。需手动调用 dispose() 方法销毁实例‌,否则会导致内存泄漏或二次渲染异常‌。

销毁 ECharts 实例

let chartDom = echarts.init('echarts容器');// 销毁实例,避免内存泄漏onBeforeUnmount(()=>{  chartDom && chartDom.dispose()})<script>
复制代码

解决 echarts 第二次无法渲染的问题

// 获取存储echarts容器的节点let chartNode = document.getElementById('chart')//移除容器上的 _echarts_instance_ 属性chartNode.removeAttribute('_echarts_instance_')
复制代码

避免多次重复初始 echarts

通过 echarts.getInstanceByDom() 检查是否已存在实例,避免重复初始化‌

<template>  <div class="chart-page">    <div class="pic" ref="chartNode"></div>  </div></template><script setup>import { ref } from 'vue'let chartNode  = ref()// echarts.getInstanceByDom的参数是页面中渲染echarts的DOM节点chartInstance = echarts.getInstanceByDom(chartNode.value);// 检查是否已存在实例if (!chartInstance) {  console.log('实例不存在')}else{  console.log('实例已存在')}<script>
复制代码

转载出处:https://www.cnblogs.com/IwishIcould/

行业拓展

分享一个面向研发人群使用的前后端分离的低代码软件——JNPF,适配国产化,支持主流数据库和操作系统。

提供五十几种高频预制组件,包括表格、图表、列表、容器、表单等,内置常用的后台管理系统使用场景和基本需求,配置了流程引擎、表单引擎、报表引擎、图表引擎、接口引擎、门户引擎、组织用户引擎等可视化功能引擎,超过数百种功能控件以及大量实用模板,使得在拖拉拽的简单操作下,也能完成开发。

对于工程师来说,灵活的使用高质量预制组件可以极大的节省时间,将更多精力花费在更有创造性和建设性的代码上。

用户头像

摸个鱼,顺便发点有用的东西 2023-06-19 加入

互联网某厂人(重生版)

评论

发布
暂无评论
Echarts与Vue3中获取DOM节点可能出现的异常错误_秃头小帅oi_InfoQ写作社区