本篇又名: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获取video
与videoRecords
的操作,并且在操作前后使用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获取video
与videoRecords
信息,isLoading
设置为true
;
第6~13行,模拟请求过程完成,更新video
与videoRecords
数据;
第14行,变量更新完成,isLoading
设置为false
。
此时,达成上述条件,<div id=’c1′></div>节点才会被渲染。设计2中的renderChart
函数在videoRecords
被赋值时触发,之后寻找该节点渲染图表;而此时由于还没有执行完成第16行修改isLoading
为false
,该节点还没有满足渲染条件,进而不会被渲染,也就自然找不到该节点,报一开始的错了。
设计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 条评论