前言
大家好,今天打算写一个vue3中的ref函数,当然不单单是封装一个ref函数这么简单,还会在此基础上引申出vue在template模板中读取及设置ref值时怎么做到省略.value的写法,要深入理解其中原理又得引申出setup函数给template暴露属性时都做了什么,从而理解三者(setup、template、ref) 之间的藕断丝连。我们这里不讲源码,旨在用最通俗的方式让大家理解其中的实现思路。
ref函数揭秘
vue官方文档 对ref是这样介绍的:如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。
对其有了基本了解后,我们创建一个myvue.js文件,尝试自己封装一个ref函数:
js
import { reactive } from 'vue'
export const ref = (value) => {
const wrapper = {
value
}
return reactive(wrapper)
}
封装好后,我们尝试在vue页面中引入使用:
js
<template>
<div>
<span>当前值:{{ count.value }}</span>
<button @click="increment">加一</button>
</div>
</template>
<script setup>
// 自己封装的ref
import { ref } from './hooks/myvue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
当我们点击加一按钮 时,页面显示的count.value值也同样能够正常刷新。But ,还没完!细心的同学可能已经发现,在template模板中使用count变量时,需要.value才行,我们想要的效果是像vue提供的ref函数 一样,不用.value也能正常读取值。
template中省略.value揭秘
其实不难 ,我们只需在前文自己封装的ref函数里增加一行代码即可!
js
import { reactive } from 'vue'
export const ref = (value) => {
const wrapper = {
value
}
/**
* 在wrapper上定义一个不可修改且不可迭代的私有属性,
* vue内部会通过这个__v_isRef私有属性判断是否是ref响应式数据
*/
// 新增代码
Object.defineProperty(wrapper, '__v_isRef', {value: true})
return reactive(wrapper)
}
这样我们就能在template中省略.value写法了!!!
到这,虽然功能实现了,但我们不能知其然而不知其所以然,下面说说其实现原理。
setup返回值暴露原理揭秘
我们这次不使用<script setup>语法糖,采用原始的setup函数暴露属性 给template模板使用:
js
import { ref } from './hooks/myvue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
// 将 ref 暴露给模板
return {
count,
increment
}
}
}
</script>
从以上可看出,setup的返回值是一个对象 ,而这个对象中的属性之所以能够在template中直接使用,是归功于webpack/vite之类的编译打包工具,在.vue文件编译成js文件中的render函数执行时,依然是通过对象.属性(比如:obj.count)的形式读取setup返回的对象属性,只不过这个setup函数返回的对象经过了Proxy包装,变成了一个响应式对象,在读取/设置 属性时,在get/set方法中拦截判断是否为ref响应式对象,如果是则会自动补全.value并返回/设置相应值。
下面我们通过编写一个proxyRefs包装函数,更直观的了解其中原理。
js
const proxyRefs = (obj) => {
return new Proxy(obj, {
get(target, key, receiver) {
// 读取target[key]的值
const value = Reflect.get(target, key, receiver)
// __v_isRef为我们标记为ref的属性
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
const value = target[key]
if(value.__v_isRef) {
// 是ref则设置.value值
value.value = newValue
// 返回设置成功
return true
}
// 设置target[key]的值,并返回是否设置成功
return Reflect.set(target, key, newValue, receiver)
}
})
}
以上就是我们实现的setup返回对象包装方法,之后我们使用ref时就不再需要.value了!主要是通过Proxy的get方法拦截读取操作,如果读取的值是ref则返回其.value值,如果不是则直接返回原始值;set拦截也是同理,目的是在设置ref值时也能省略.value写法。
下面我们通过一个例子测试一下:
js
<script>
import { ref, proxyRefs } from './hooks/myvue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
const state = {
count,
increment
}
const state2 = proxyRefs(state)
console.log(state2.count) // 0
state2.count++
console.log(state.count.value) // 1
// 将 ref 暴露给模板
return state
}
}
</script>
以上我们把setup返回对象通过刚刚我们自己写的proxyRefs方法做一层包装,并赋值给state2,并且我们通过state2.count成功读取和设置了state中的count值。
总结
首先,封装ref函数,并创建一个具有value属性wrapper对象,并把传入的参数赋值给value属性,最后再把wrapper对象通过reactive响应式API,包装成一个深层的响应式对象。
其次,为了使ref能够省略.value的写法,我们给wrapper包装对象设置了一个私有属性__v_isRef,由于vue内部对setup函数返回的对象做了Proxy拦截包装,所以在template模板中读取ref的值时,可以省略.value读取及设置ref的值。
最后,我们手写了一个在读取或设置 ref的值时,可以省略.value的包装函数proxyRefs,深入了解setup函数给模板暴露属性时,是如何包装返回的对象给模板使用。