本篇又名:Vue中mounted之后就一定能getElementById获取到DOM节点了么?

需求

天钿Daily中的视频详情页面,访问时根据aid从api获取视频信息以及历史数据,如果系统中不存在这个视频则显示“不存在”,否则显示视频信息,以及根据历史数据,绘制图表。

设计1

听上去是个很简单的需求。根据习惯,使用ajax调用API部分放在created钩子里,绘制图表放在mounted钩子里。很快,第一版设计就完成了。

<!DOCTYPE html>
<head>
  <title>mount test</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script src="https://gw.alipayobjects.com/os/lib/antv/g2/3.5.11/dist/g2.min.js"></script>
</head>
<body>
  <div id='app'>
    <div v-if='isLoading'>
      <p>Loading...</p>
    </div>
    <div v-else>
      <div v-if='video === null'>
        <p>Fail to load video!</p>
      </div>
      <div v-else>
        <p>video info {{ video }} </p>
        <p>history view chart:</p>
        <div id='c1'></div>
      </div>
    </div>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: function() {
        return {
          isLoading: false,
          video: null,
          videoRecords: null
        }
      },
      methods: {
        renderChart: function() {
          // render chart
          const chart = new G2.Chart({
            container: 'c1',
            width: 600,
            height: 300,
          });
          chart.source(this.videoRecords);
          chart
            .line()
            .position('added*view')
          chart.render();
        }
      },
      created: function() {
        let that = this;
        that.isLoading = true;
        setTimeout( function() { 
            // after 1000ms get video and videoRecords from api
            that.video = {aid: 456930, title: '前尘如梦'};
            that.videoRecords = [
              {added: 1, view: 1},
              {added: 2, view: 2},
              {added: 3, view: 3},
              {added: 4, view: 4},
              {added: 5, view: 5}
            ];
            that.isLoading = false;
          }, 1000); 
      },
      mounted: function() {
        this.renderChart();
      }
    });
  </script>
</body>

页面结构与变量都进行了简化以方便理解。这里created钩子里模拟了一次耗时1000ms的从API获取videovideoRecords的操作,并且在操作前后使用isLoading变量标注,而mounted钩子里调用renderChart函数绘制图表。HTML部分,使用v-if v-else语法,实现逻辑:当在加载时显示“Loading…”,加载完后判断video是否为空,若为空显示“Fail to load video”,若不为空则显示video的详细信息,以及根据videoRecords绘图。

在Chrome中运行,并没有看到图表渲染出来,控制台报错:Error: Please specify the container for the chart!

这里并没有去关注报错信息,而是突然意识到一个问题:created中,从API获取数据需要时间,而mounted中绘图时videoRecords还没有得到,依旧是null。修改代码,于是有了设计2。

设计2

使用监听器,监听videoRecords变量,当其发生变化时调用renderChart函数绘图。

mounted: function() {
  // this.renderChart(); // design 1
},
watch: {
  videoRecords: function() {
    this.renderChart(); // design 2
  }
}

在Chrome中运行,报了一样的错。很显然,该错误的原因不在此。

分析:即使绘图时数据源videoRecords为null,G2因为缺少源数据无法绘制图表,但是并不会报错。阅读错误提示,才明白错误问题在于:没有找到绘图的容器

分析

观察renderChart函数

renderChart: function() {
    // render chart
    const chart = new G2.Chart({
        container: 'c1',
        width: 600,
        height: 300,
    });
    chart.source(this.videoRecords);
    chart
        .line()
        .position('added*view')
    chart.render();
}

参考G2文档了解到,创建chart时,container指定渲染的图表的id,此处通过document.getElementById('c1')获取渲染目标DOM节点。猜测可能是因为此时document.getElementById('c1')无法获取该元素,所以报错。

为了验证该猜测,修改代码,测试此时是否能通过document.getElementById('c1')获取元素,在创建G2对象前加入此句:

console.log('c1: ' + document.getElementById('c1')); // check c1

运行,输出c1: null。果不其然,获取不到该DOM姐节点。

根据Vue生命周期图示,mounted钩子在挂载完毕之时被触发,此时所有节点应该都已经挂载到<div id=’app’>…</div>中了,为什么c1获取不到呢?

是因为获取到videoRecords之后,还没有完成挂载么?为验证该猜测,在mounted钩子中加入log。

mounted: function() {
  console.log('mounted!');
}

在Chrome中运行,观察控制台,可以发现在document.getElementById('c1')执行之前,mounted钩子就已经执行了。也就是说,绘制图表时已经完成挂载,但是那一时刻,就是没有c1这个节点。

顿悟

那么,这个DOM节点去哪儿了呢?不是已经mounted了么?Chrome里查看源码也确实存在的呀?那到底是什么时候出现的呢?

回头再看看HTML代码,顿时发现了问题。

<div id='app'>
  <div v-if='isLoading'>
    <p>Loading...</p>
  </div>
  <div v-else>
    <div v-if='video === null'>
      <p>Fail to load video!</p>
    </div>
    <div v-else>
      <p>video info {{ video }} </p>
      <p>history view chart:</p>
      <div id='c1'></div>
    </div>
  </div>
</div>

这里使用了Vue中的条件渲染。文档开头是这样介绍的:

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

也就是说,只有条件满足的时候,这个块中的所有元素才会被渲染。查看此处HTML代码的逻辑,可以发现,<div id=’c1′></div>节点只有在isLoading为false且video不为null的时候才会被渲染。

追踪JS代码中isLoading的值的变化,在created钩子中,代码如下:

created: function() {
  let that = this;
  that.isLoading = true;
  setTimeout( function() { 
      // after 1000ms get video and videoRecords from api
      that.video = {aid: 456930, title: '前尘如梦'};
      that.videoRecords = [
        {added: 1, view: 1},
        {added: 2, view: 2},
        {added: 3, view: 3},
        {added: 4, view: 4},
        {added: 5, view: 5}
      ];
      that.isLoading = false;
    }, 1000); 
},

第3行,即将开始调用API获取videovideoRecords信息,isLoading设置为true

第6~13行,模拟请求过程完成,更新videovideoRecords数据;

第14行,变量更新完成,isLoading设置为false

此时,达成上述条件,<div id=’c1′></div>节点才会被渲染。设计2中的renderChart函数在videoRecords被赋值时触发,之后寻找该节点渲染图表;而此时由于还没有执行完成第16行修改isLoadingfalse该节点还没有满足渲染条件,进而不会被渲染,也就自然找不到该节点,报一开始的错了。

设计3

现在问题已经很明朗了,解决方法也很简单:只需要保证该节点被渲染后再渲染图表即可。这里我项目中的实现方案是:使用组件封装该图,传入videoRecords作为参数,当该组件被渲染时,即mounted钩子中,对图表进行渲染。封装了的组件如下:

Vue.component('view-chart', {
  props: [
    'videoRecords'
  ],
  mounted: function() {
    const chart = new G2.Chart({
      container: 'c1',
      width: 600,
      height: 300,
    });
    chart.source(this.videoRecords);
    chart
      .line()
      .position('added*view')
    chart.render();
  },
  template: '<div id="c1"></div>'
})

去除原先对videoRecords的watch以及methods中的renderChart函数,原先的div替换为封装好的组件:

<view-chart :video-records='videoRecords'></view-chart>

最终效果如下:

现在可以回答最开始的问题了。Vue中mounted之后就一定能getElementById获取到DOM节点了么?是的!前提是该节点并没有被别的条件限制导致无法渲染。

本次使用的文件可以在这里找到。


0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注