echarts 在 vue2 中的顯示問題

Tinypan發表於2024-06-26

最簡單的 v-show

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script type="importmap">
      {
        "imports": {
          "echarts": "http://localhost:8080/FrontEnd/assets/js/echarts/echarts.esm.js",
          "vue": "http://localhost:8080/FrontEnd/assets/js/vue2/vue.esm.browser.js"
        }
      }
    </script>

    <style>
      .example {
        width: 50%;
        height: 500px;
        border: 1px solid red;
      }
      [v-cloak] {
        display: none;
      }
    </style>
  </head>
  <body>
    <div id="app" v-cloak>
      <button @click="showEchart">{{show}}</button>
      <div ref="chart-container" class="example" id="main" v-show="show"></div>
    </div>

    <script type="module">
      import * as echarts from 'echarts';
      import Vue from 'vue';

      new Vue({
        el: '#app',
        data() {
          return {
            show: false,
            myChart: null,
            options: {
              title: {
                text: 'ECharts 入門示例',
              },
              tooltip: {},
              xAxis: {
                data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'],
              },
              yAxis: {},
              series: [
                {
                  name: '銷量',
                  type: 'bar',
                  data: [5, 20, 36, 10, 10, 20],
                },
              ],
            },
          };
        },
        methods: {
          showEchart() {
            this.show = !this.show;

            // 等待容器掛載完成
            this.$nextTick(() => {
              // 容器為百分比大小,顯示的時候重新計算獲取具體畫素值
              this.myChart.resize();
            });
          },
        },
        mounted() {
          // 由於容器已經在 dom tree 中,可以直接獲的容器
          this.myChart = echarts.init(this.$refs['chart-container']);
          this.myChart.setOption(this.options);
        },
      });
    </script>
  </body>
</html>

來個騷操作

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script type="importmap">
      {
        "imports": {
          "echarts": "http://localhost:8080/FrontEnd/assets/js/echarts/echarts.esm.js",
          "vue": "http://localhost:8080/FrontEnd/assets/js/vue2/vue.esm.browser.js"
        }
      }
    </script>

    <style>
      .example {
        width: 50%;
        height: 500px;
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div ref="chart-container" class="example" id="main"></div>
    </div>

    <script type="module">
      import * as echarts from 'echarts';
      import Vue from 'vue';

      let options = {
        title: {
          text: 'ECharts 入門示例',
        },
        tooltip: {},
        xAxis: {
          data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'],
        },
        yAxis: {},
        series: [
          {
            name: '銷量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20],
          },
        ],
      };

      echarts.init(document.getElementById('main')).setOption(options);

      setTimeout(() => {
        new Vue({
          el: '#app',
          data() {
            return {
              options: {
                title: {
                  text: 'ECharts 入門示例',
                },
                tooltip: {},
                xAxis: {
                  data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'],
                },
                yAxis: {},
                series: [
                  {
                    name: '銷量',
                    type: 'bar',
                    data: [5, 20, 36, 10, 10, 20],
                  },
                ],
              },
            };
          },
        });
      }, 1000);
    </script>
  </body>
</html>

一開始有 echarts 渲染,1s 後畫布被清空,這個和 Vue 的渲染原理有關

<div id="app">
  <div ref="chart-container" class="example" id="main"></div>
</div>

Vue 在處理這段 html 時,將 html 字串作為模版在 Vue 內部解析,將{{msg}},v-on:click 等處理,然後在渲染時,不是使用原來的元素,而是建立同名元素,將原來元素的所有特性複製到新元素上,掛載的是新元素。

首先 echarts 處理完成後,dom tree 為

<div id="app">
  <div
    ref="chart-container"
    class="example"
    id="main"
    style="user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); position: relative;"
    _echarts_instance_="ec_1719372515943"
  >
    <div
      style="position: relative; width: 727px; height: 500px; padding: 0px; margin: 0px; border-width: 0px; cursor: default;"
    >
      <!--  canvas 畫布有內容 -->
      <canvas
        data-zr-dom-id="zr_0"
        width="1454"
        height="1000"
        style="position: absolute; left: 0px; top: 0px; width: 727px; height: 500px; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); padding: 0px; margin: 0px; border-width: 0px;"
      ></canvas>
    </div>
    <div class=""></div>
  </div>
</div>

交給 Vue 處理,Vue 只是建立同名元素,比如 canvas,但並未在新的 canvas 上畫東西,所以 Vue 處理完成,掛載到 DOM 上時,畫布是空白的。

既然是新的 canvas 替換了已經畫過東西的 canvas,那麼我可以在新的 canvas 上重新畫嗎?

new Vue({
  mounted() {
    echarts.init(document.getElementById('main')).setOption(options);
  },
});

不行。因為 echarts 對容器有快取

檔案:echarts/src/core/echarts.ts

const DOM_ATTRIBUTE_KEY = '_echarts_instance_';

export function getInstanceByDom(dom: HTMLElement): EChartsType | undefined {
  return instances[modelUtil.getAttribute(dom, DOM_ATTRIBUTE_KEY)];
}

export function init(
  dom?: HTMLElement | null,
  theme?: string | object | null,
  opts?: EChartsInitOpts
): EChartsType {
  const existInstance = getInstanceByDom(dom);
  if (existInstance) {
    if (__DEV__) {
      warn('There is a chart instance already initialized on the dom.');
    }
    return existInstance;
  }
}
class ECharts {
  dispose() {
    const dom = this.getDom();
    if (dom) {
      // 透過屬性值清快取
      modelUtil.setAttribute(this.getDom(), DOM_ATTRIBUTE_KEY, '');
    }
  }
}

那就清了 echarts 快取,重新畫

new Vue({
  mounted() {
    echarts.dispose(document.getElementById('main'));
    echarts.init(document.getElementById('main')).setOption(options);
  },
});

稍微複雜點的 v-if

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script type="importmap">
      {
        "imports": {
          "echarts": "http://localhost:8080/FrontEnd/assets/js/echarts/echarts.esm.js",
          "vue": "http://localhost:8080/FrontEnd/assets/js/vue2/vue.esm.browser.js"
        }
      }
    </script>

    <style>
      .example {
        width: 50%;
        height: 500px;
        border: 1px solid red;
      }
      [v-cloak] {
        display: none;
      }
    </style>
  </head>
  <body>
    <div id="app" v-cloak>
      <button @click="showEchart">{{show}}</button>
      <div ref="chart-container" class="example" id="main" v-if="show"></div>
    </div>

    <script type="module">
      import * as echarts from 'echarts';
      import Vue from 'vue';

      new Vue({
        el: '#app',
        data() {
          return {
            show: false,
            myChart: null,
            options: {
              title: {
                text: 'ECharts 入門示例',
              },
              tooltip: {},
              xAxis: {
                data: ['襯衫', '羊毛衫', '雪紡衫', '褲子', '高跟鞋', '襪子'],
              },
              yAxis: {},
              series: [
                {
                  name: '銷量',
                  type: 'bar',
                  data: [5, 20, 36, 10, 10, 20],
                },
              ],
            },
          };
        },
        methods: {
          showEchart() {
            this.show = !this.show;

            // 導致重複渲染,建議用 v-show
            this.$nextTick(() => {
              if (this.show) {
                // 每次都是一個新的 div.chart-container 元素
                this.myChart = echarts.init(this.$refs['chart-container']);
                this.myChart.setOption(this.options);
              } else {
                echarts.dispose(this.myChart);
                this.myChart = null;
              }
            });
          },
        },
        mounted() {
          if (this.$refs['chart-container']) {
            this.myChart = echarts.init(this.$refs['chart-container']);
            this.myChart.setOption(this.options);
          }
        },
      });
    </script>
  </body>
</html>

相關文章