揭秘ref的背后——真滴没有那么难
前言
使用过vue3的同学应该对于ref
并不陌生,
但是不知道大家有没有想过vue3的Composition API
中已经有了reactive
为什么还要再加一个ref
呢?那么本着知其然知其所以然的原则本文将从以下几个方面展开对ref
的研究。
Tips
不知道大家在使用ref
的时候有没有这样的一个困惑,就是输出ref
值的时候信息量太大,像这样每次还需要将其展开才能看到我们设置的值,很是不便。
那么有没有一种方法能将其简化呢?其实vue有考虑到这一点,我们只需要将Chrome的DevTools
中的Enable custom formatters
这一选项勾选上就会发现神奇的一幕。
为什么要入ref
vue3中使用Proxy
来进行数据劫持,但是Proxy
并不支持对原始值(基本数据类型)进行劫持,那如果想要声明一个原始值的响应式数据该怎么办呢?这就是为什么要引入ref
的原因,因为要实现对原始值的数据响应式。
ref实现
了解了ref
出现的原因后,下面来实现一个简单的ref
。因为Proxy
只支持对非原始值(引用类型)的数据劫持,那这时候我们可以用非原始值类型去包裹一下原始值,然后再使用reactive
包裹完成数据的响应式,ref
其实就是借助reactive
实现的。
通过观察ref
的基本结构我们可以得出:
- 通过
ref
包裹的数据是响应式的 - 会返回一个包含
value
属性的Ref对象
于是就可以写出以下代码:
function myRef(val) {
let obj = {
value: val,
}
return reactive(obj);
}
const count = myRef(0)
setTimeout(() => {
count.value = 1 // 视图也会同步更新
}, 1000)
这样也能按照预期工作,但是现在由于是使用reactive
来实现的ref
,如何对它们进行区分呢?也就是如何区分ref
和reactive
声明的数据,其实也很简单,无非是做判断,在vue源码中使用了__v_isRef
来做判断,这里我们就不写那么复杂,我们可以在myRef
函数中使用Object.defineProperty
方法给obj
定义一个不可枚举且不可写的属性__v_isRef
并且值为true,这样就可以通过检查对象中是否有__v_isRef
属性来判断是否是一个ref
数据了。
function myRef(val) {
let wrap = {
value: val
}
Object.defineProperty(obj, "__v_isRef", {
value: true
})
return reactive(wrap)
}
ref解决数据响应丢失的问题
ref还可以解决数据响应丢失的问题,当我们在setup
中将数据暴露到模板中时如果使用了扩展运算符,像这样:
setup() {
const obj = reactive({
name: "xxx",
age: 20
})
return {
...obj
}
}
然后直接在模版中使用obj
中的属性会丢失其响应式,也就是说修改obj
这个响应式数据时视图并不会重新渲染。
setup() {
const obj = reactive({
name: "xxx",
age: 20
})
setTimeout(() => {
obj.name = "sss"
}, 1000)
return {
...obj
}
}
这是由于使用了扩展运算符导致的,在 setup()
函数中返回的对象会暴露给模板和组件实例,而使用了扩展运算符就相当于是返回了一个普通对象,它并不具备任何响应式能力。
return {
...obj
}
// 等价于
return {
name:"xxx",
age:20
}
所以要解决这个问题这里的关键点就在于如何将返回的普通对象与响应式数据关联起来,无疑应该使用代理。
const obj=reactive({
name:"xxx",
age:20
})
let newObj={
name: {
get value() {
return obj.name
}
},
age: {
get value() {
return obj.age
}
}
}
我们可以把newObj
看作是要返回的对象,让newObj
拥有与obj相同的属性并且每个属性值都是一个对象,然后定义一个名为value
的getter方法返回obj
中对应的属性值,那么当读取value时其实读取的是obj
对象下对应的属性值,这样一来就实现了普通对象与响应式数据之间的关联。
简化一下上面的代码就变成了toRef
函数:
function toRef(obj,key){
let newObj={
get value(){
return obj[key]
}
}
return newObj
}
let newObj = myToRef(obj,"name")
console.log(newObj.value) // "xxx"
但是这样写未免太麻烦了,如果需要对一个对象中的多个属性进行转换的话就要写大量的toRef
,于是就诞生了toRefs
,它可以帮我们完成对一个对象的转换。实现它也很简单,只需要循环取出对象中的key然后依次进行toRef
即可。
function toRefs(obj) {
let newObj = {};
for (let key in obj) {
newObj[key] = toRef(obj, key)
}
return newObj;
}
let newObj = toRefs(obj)
console.log(newObj.name.value) // "xxx"
到此为止响应式丢失的问题就已经解决了,因为toRef
和toRefs
返回的都是一个ref
数据,所以还需要加上一个前面提到过的__v_isRef
标识。
function toRef(obj,key){
let newObj={
get value(){
return obj[key]
}
}
Object.defineProperty(newObj, '__v_isRef', {
value: true
})
return newObj
}
自动脱ref
当我们在模板中使用ref
时并不需要添加value属性,但是根据上面的例子来看访问属性都必须带上value属性,这会增加用户的心智负担因为我们通常习惯直接在模板中直接使用数据。那么想要实现这个功能还是要用到代理。
function proxyRefs(target) {
return new Proxy(target, {
get(target, key) {
let val = target[key]
return val.__v_isRef ? val.value : val
}
})
}
const newOobj = toRefs(obj)
const data = proxyRefs(newObj)
console.log(data.name) // xxx
只需要判断属性值是否有__v_isRef
这个标识,有的话加上.value
没有的话直接返回,是不是感觉挺简单!
转载自:https://juejin.cn/post/7359084920583094322