Vue组件中$refs引用失效问题咨询(非单文件组件场景)
问题:Vue组件mounted钩子中访问$refs.dial_canvas返回undefined?
我写了一段简化的Vue代码,想用自定义组件渲染一个表盘,但是运行后控制台显示this.$refs.dial_canvas是undefined,连$el也变成了undefined。我已经注意到不在created里访问refs,而是用了mounted,但还是不行。另外我不想用.vue文件和服务端编译的方式,只想用静态文件+最简Web服务器,组件要能复用展示不同值。
我的代码如下:
<html> <head> <script src="file:///D:/OtherWork/javascript/vue/vue.js"></script> </head> <body> <div id="app"> <Dial style="width: 120px; height: 120px;" v-model="src_value"></Dial> </div> <script> Vue.component('Dial', { template: '<canvas ref="dial_canvas"></canvas>', data: function () { return { value: 0, centre: { x: 0, y: 0 }, canvas: null } }, props: ['value'], mounted: function() { console.log(this.$refs.dial_canvas); }, render: function() { // TODO: Draw on canvas here. } }); var app = new Vue({ el: '#app', data: { src_value: -1 }, created() { } }); </script> </body> </html>
问题原因与解决方法
兄弟,你这问题其实踩了两个关键的坑:
1. 同名的props和data变量冲突
你在组件里同时声明了props: ['value']和data里的value:0,这俩重名了!Vue里组件的props和data绝对不能用相同的名字,会直接导致变量覆盖,还会打乱组件的响应式逻辑,这是第一个隐患。
2. render函数直接覆盖了template模板
Vue组件里如果同时写了template和render函数,render函数会优先生效!你现在的render函数只留了个TODO注释,根本没有渲染任何元素,所以页面上其实完全没生成canvas标签,自然$refs.dial_canvas和$el都是undefined了。
修正后的代码
根据你的需求(静态文件部署、组件可复用),我给你调整了代码,直接解决这两个问题:
<html> <head> <script src="file:///D:/OtherWork/javascript/vue/vue.js"></script> </head> <body> <div id="app"> <Dial style="width: 120px; height: 120px;" v-model="src_value"></Dial> </div> <script> Vue.component('Dial', { template: '<canvas ref="dial_canvas"></canvas>', data: function () { return { // 把data里的value改成localValue,避免和props的value冲突 localValue: 0, centre: { x: 0, y: 0 }, canvas: null } }, // 实现v-model必须的props和事件逻辑 props: ['value'], watch: { // 监听props的value变化,同步到本地变量并触发重绘 value(newVal) { this.localValue = newVal; this.drawDial(); } }, mounted: function() { this.canvas = this.$refs.dial_canvas; // 给canvas设置实际宽高(避免样式宽高导致绘制模糊) this.canvas.width = 120; this.canvas.height = 120; this.centre.x = this.canvas.width / 2; this.centre.y = this.canvas.height / 2; // 现在可以正常拿到canvas了,打印看看 console.log(this.canvas); // 初始化绘制表盘 this.drawDial(); }, methods: { drawDial() { if (!this.canvas) return; const ctx = this.canvas.getContext('2d'); // 清空画布 ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制表盘外圈 ctx.beginPath(); ctx.arc(this.centre.x, this.centre.y, 50, 0, Math.PI * 2); ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.stroke(); // 根据value绘制指针(这里做简单示例,取值范围0-100) const normalizedValue = Math.max(0, Math.min(100, this.localValue)); const angle = (normalizedValue / 100) * Math.PI * 2 - Math.PI / 2; ctx.beginPath(); ctx.moveTo(this.centre.x, this.centre.y); ctx.lineTo( this.centre.x + Math.cos(angle) * 40, this.centre.y + Math.sin(angle) * 40 ); ctx.strokeStyle = '#f00'; ctx.lineWidth = 3; ctx.stroke(); } } // 删掉无效的render函数,让template正常生效;如果一定要用render,可以看下面的补充 }); var app = new Vue({ el: '#app', data: { src_value: 50 // 改成正数方便看效果 }, created() { } }); </script> </body> </html>
关键调整点说明:
- 把data里的
value改成localValue,避免和props的value重名,同时通过watch监听props变化,同步数据并触发重绘。 - 删除了无效的render函数,让template里的canvas正常渲染,这样mounted里就能顺利拿到
$refs.dial_canvas了。 - 给canvas设置了实际宽高,避免样式宽高导致绘制模糊的问题。
- 添加了
drawDial方法统一管理绘制逻辑,初始化和数据变化时都能调用。
如果之后你确实想用render函数代替template,可以这样写(替换掉原来的template):
render(h) { return h('canvas', { ref: 'dial_canvas', attrs: { width: 120, height: 120 } }); }
这样也能正确生成带ref的canvas元素,mounted里同样能拿到refs。
现在运行代码,你应该就能在控制台看到canvas元素,而且表盘也能正常渲染了~
内容的提问来源于stack exchange,提问作者user526477
相关产品推荐
相关产品推荐

