JS就是一门这样的语言,你无时不刻想骂娘,但是不知不觉中,你就也写出了当年骂娘的语法,并且窃喜到:这个磨人的小妖精。
后记:这篇博文,最贴切的名字,应该是,在VUE3中的setUp方法中,如何拿到dom的ref引用。
再次后记
终于在官方文档中读到了这种用法的出处:
在虚拟 DOM 补丁算法中,如果 VNode 的 ref 键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
分割线
在一个vue2项目里,瞄到了同事一行代码,大意如下:
//模板
<template>
<div>
<qhse-detail ref="detailRef" />
<div @click="detailRef.onView(row.id)"></div>
</div>
<template>
//js
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from '@vue/composition-api'
export default defineComponent({
setup(props) {
const detailRef = ref<{ onView?: (id: string) => void }>()
return { detailRef }
}
})
</script>
通过composition-api,使可以使用vue3中的api。
当时看到const detailRef = ref<{ onView?: (id: string) => void }>()
这行代码的时候,是很蒙圈的。因为对ref()方法的认知比较浅显,结合官网和一干博文关于ref()
,reactive()
,toRef()
,toRefs
等API的介绍,我的认知还停留在ref()
肯定会有入参这一观念上。
ref()
首先明确一点,在setup方法中,vue中的this
是还没有初始化出来的。而在模板中绑定的内部数据,调用的内部方法以及一干响应式的效果,都是基于this
的。对于那些在this
还为初始化完成就想在模板中使用的数据,vue2中要使用类似于Vue.set( target, propertyName/index, value )
或者Object.assign(this.obj, { d: 4 })
这样的API。
而在setup
方法中,最后的return语句包含的属性,可以在this还没有被初始化出来的时候就可以使用(这个表述我自己也存疑。PS:后记,这句话确实不准确,最新的解释是,在setup方法中,返回的属性,不用显示的声明在vuedata中,就可以通过this访问。),并且在this
初始化之后,同名属性也会被包含在this
中。
分割线
<div @click="demoClick" style="height: 100px; width: 100px; background: red">
{{ demoRef }}
</div>
setup(props) {
const demoRef = ref(0)
setTimeout(() => {
//这里要通过value改变值
demoRef.value = 1
//demoRef在这里,是object
console.log(typeof demoRef)
}, 1000)
},
methods: {
demoClick() {
console.log(this.demoRef)
//demoRef在这里,是number了!!!!
console.log(typeof this.demoRef)
//所以,改变demoRef的值,直接赋值
this.demoRef = 2
}
}
上面代码,在setup方法中,创建了demoRef,并且绑定到页面上,页面渲染成功后,div中的值是0,此时demoRef是一个对象;延迟执行的方法中,要想改变demoRef的值,要通过value,页面同时变成1。在点击事件中,demoRef的类型就已经是number了,改变它的值,直接赋值就可,页面也跟随变化。由于不了解原理,不清楚ref()底层到底干了什么,只能通过表象来观察。
但是这里不管怎么说,都在刻意理解的范畴中,不管它底层做了什么,都是建立了代理和监听,实现了响应式的效果。紧接着,奇怪的现象在下面:
奇怪的现象
<div ref="demoRef" @click="demoClick" style="height: 100px; width: 100px; background: red">
{{ demoRef }}
</div>
setup(props) {
//只要模板中,如果节点或者组件的ref属性,有和这个变量名称相同的,那么demoRef刚初始化出来,其value属性,就已经是一个dom节点或者是vue组件
const demoRef = ref(0)
setTimeout(() => {
//这里要通过value改变值--然而并没有卵用,它并不会改变
demoRef.value = 1
//demoRef在这里,依然是RefImpl类型的object,只不过它的value,已经是一个dom节点!!!
console.log(typeof demoRef)
}, 1000)
},
methods: {
demoClick() {
console.log(this.demoRef)
//demoRef在这里,是div类型的dom节点了!!!!
console.log(typeof this.demoRef)
//赋值也没用---!!!
this.demoRef = 2
}
}
只需要在模板中加上和ref()
函数返回值变量名称相同的ref属性,就翻了天了。首先,demoRef刚初始化完成,它依然是个RefImpl,但是它的value属性值,是div dom节点!!!(模板中的ref属性,如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例);因为div有了ref属性,所以,在this.$refs
上,会有一个名为demoRef的dom节点引用;同时,因为在setup
方法中,return了demoRef,所以,在this
实例上也有一个名为demoRef的属性,它是一个和this.$refs.demoRef
一模一样的对象;这两个地方的demoRef,是一个互为影子一样的存在。如下图:
也就是当模板中,有同名ref属性的节点存在时,ref()
函数入参,不管是啥,是0也好,是一个亿也好,是基本数据也好,是对象也好,甚至像同事写的不传参数,或者索性传null也好,都是徒劳,人家压根不会考虑,直接就把ref属性代表的节点或者组件引用赋值给RefImpl的value了。这种通过函数返回值变量名和模板ref属性之间弱的不能再弱的关联,所产生的的奇怪效应,真实让人心里默念一万个MMP!而且奇葩的是官网上,是没有找到有关这种使用方法的介绍的,所以,这究竟是一种奇技淫巧,还是底层故意设计的,还是恰巧能跑的经典案例,不得而知。但是总而言之,它就这样存在着,还希望有缘人度化一二。
只要将模板ref改个名字,或者ref()
返回值变量名称改一下,ref
还是ref
,ref()
还是ref()
,一切恢复如初:
补充于2022-04-21
今天一度感觉遇上鬼了。后来也只想通了一半。
基于前述观察到的现象,我始终确信了,当const aRefName = ref(null)
,在setUp中初始化的时候,它的value值就已经是ref
所属性所标记的dom节点或者组件实例了。因为在第一次写这个博文的时候,是通过console观察无误的,他的值如下图所示:
如上图所示,抛开所有,不管咋说,ref方法返回的值,就是一个正经八百的对象,它有一个value属性,从console里展开value属性,也确实是一个vue组件实例,项目里,大量的代码,也确实是这么写的。
直到今天,我想做一个操作:既然之前我把这种写法,理解成,是在this还没初始化之前,通过特殊的写法,拿到dom或者vue实例的引用,那么理论上,我就可以在setUp方法中,通过使用这种方式拿到的引用,调用vue组件实例的一些方法,个中细节,它应该帮我去处理,结果却被啪啪打脸。(现在想想当然是不合适了,还没有渲染出来,就去拿人家的值,当然不合适,所以今天碰到的鬼,原因是基于在console里观察到,formRef初始化的时候,value属性确实是有值的。)
我的写法如上图所示,我想通过doFormFillUp()
方法,调用formRef中vaule属性中的重置方法,结果哐哐报错。
在console中打印formRef
,发现这货确实是有value值的啊,这货的value属性就是form组件实例啊(上上一张图)。
然后我又打印了它的value值,我擦,value的值,竟然不是不是Vue组件实例的了,恐怖。。。
真是惨绝人寰,让人无言以对。
好好的一个value值,上一行还是一个VueComponent,下一行就变成了最开始的初始值5。
我能怎么理解?我只能理解成,这特喵的是Vue的bug,或者是浏览器的bug,汗…
出现这种现象,只有在子组件第一次初始化的时候,所以,对项目中大量出现的这种写法,只要保证,在首次渲染完成之前,没有使用通过这种方式拿到的引用,去搞事情,倒是并没有太大的影响。
我擦,又又又涨知识了,这一天天的,都是嘛呀!所以总结为:不要过分相信console打印出的结果,还是要结合底层的逻辑,去设计自己的代码。