TL;DR
扩展 Vue3 的 toRefs API。
学前知识
toRef 处理 reactive 无法解构
一般来讲复杂数据类型,我们都是通过 reactive 来管理的,就像下面这样。
<script setup>
import { reactive } from "vue";
const user = reactive({ name: "Vue3", age: 1 });
</script>
当数据结构复杂,我们在读取数据的时候链式打点很不方便和直观,比如:
const user = reactive({
data: {
name: "Vue3",
age: 1,
hobby: ["basketball", "football", "music", "dance"]
}
});
const name = user.data.name 返回字符串 Vue3。除了链式打点赋值 还可以使用解构赋值const { name } = user.data。
但是无论是解构赋值还是链式打点赋值,新定义的变量都没有响应特性,原因很好理解,因为没有对新创建的变量进行响应式代理。
接下来我们很自然想到通过 ref API 对想要获取的值进行响应式代理:
const name = ref(user.data.name);
// or
const { name } = user.data;
const newName = ref(name);
上面这种情况值得说说:
- 假如 ref 的是基本数据类型,基本类型赋值给变量,会创建一个新的地址存储值。源数据和新数据完全没关系了。此时修改 reactive 的值是无法自动关联到新的变量,修改新的变量源数据也不会自动响应,因为变量就是一个纯粹的值,基本类型情况下丧失了数据的关联性。
- 假如 ref 的复杂数据类型,复杂类型赋值给变量,并不会创建一个新的地址,而是新老变量都指向同一个地址,所以新老数据彼此是关联的,比如下面例子:
<script setup>
import { reactive, ref } from "vue";
const user = reactive({
data: {
name: "Vue3",
age: 1,
hobby: ["basketball", "football", "music", "dance"]
}
});
const refDance = ref(user.data.hobby);
</script>
<template>
<h1>ref dance: {{ refDance[3] }}</h1>
<h1>reactive dance: {{ user.data.hobby[3] }}</h1>
<label>
change reactive dance:
<input type="text" v-model="user.data.hobby[3]" />
</label>
<label>
change ref dance:
<input type="text" v-model="refDance[3]" />
</label>
</template>
可见使用 ref 给值设置响应式特性并不是理想的解决办法,那怎么解决呢,你想,可以对地址代理,地址所在地不就是位置吗,这思路不就来了,不对读取值进行代理,而是直接代理 reactive 给的 key 的所在位置,即给一个位置创建响应式特性。
import { isRef } from "vue";
class ObjectRefImpl {
#object;
#key;
constructor(_object, _key) {
this.#object = _object;
this.#key = _key;
}
get value() {
return this.#object[this.#key];
}
set value(newVal) {
this.#object[this.#key] = newVal;
}
}
function toRef(object, key,) {
const val = object[key];
// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
return isRef(val) ? val : new ObjectRefImpl(object, key);
}
ref 对值进行代理,toRef 对位置进行代理,因为这个区别,这也就是为什么 ref 使用 RefImpl 类实现 toRef 使用 ObjectRefImpl 类实现。
我们测试下能不能运行,案例代码如下:
<script setup>
import { reactive, isRef } from "vue";
class ObjectRefImpl {
#object;
#key;
constructor(_object, _key) {
this.#object = _object;
this.#key = _key;
}
get value() {
return this.#object[this.#key];
}
set value(newVal) {
this.#object[this.#key] = newVal;
}
}
function toRef(object, key,) {
const val = object[key];
// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
return isRef(val) ? val : new ObjectRefImpl(object, key);
}
const user = reactive({
name: "Vue3",
age: 1,
hobby: ["basketball", "football", "music", "dance"]
});
const name = toRef(user, "name");
</script>
<template>
<h1>ref name: {{ name.value }}</h1>
<h1>reactive name: {{ user.name }}</h1>
<label>
change reactive name:
<input type="text" v-model="user.name" />
</label>
<label>
change ref name:
<input type="text" v-model="name.value" />
</label>
</template>
不用想了,效果非常好,其实 Vue3 的 toRef() API 就干了这么一件事,查看 toRef 源码,发现函数还有第三个参数增加了默认值,这点逻辑难度对你肯定不难。
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
下面是 ObjectRefImpl 类的实现:
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
通往罗马的路不止一条
对 reactive 的 key 进行位置代理我们实现了一个 ObjectRefImpl 类,除了自己实现还可以利用 Vue3 提供的 computed 的 get 和 set 实现。
function toRef(object, key,) {
const val = object[key];
// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
return isRef(val) ? val : computed({
get: () => {
return object[key];
},
set: (newValue) => {
object[key] = newValue;
}
});
}
VueUse 扩展 toRefs 选择的就是这条路。
同样运行上面的案例测试:
<script setup>
import { reactive, isRef, computed } from "vue";
function toRef(object, key,) {
const val = object[key];
// 如果是 ref 就是原样返回,否则给 key 所在位置创建响应式
return isRef(val) ? val : computed({
get: () => {
return object[key];
},
set: (newValue) => {
object[key] = newValue;
}
});
}
const user = reactive({
name: "Vue3",
age: 1,
hobby: ["basketball", "football", "music", "dance"]
});
const name = toRef(user, "name");
console.log(name);
</script>
<template>
<h1>ref name: {{ name }}</h1>
<h1>reactive name: {{ user.name }}</h1>
<label>
change reactive name:
<input type="text" v-model="user.name" />
</label>
<label>
change ref name:
<input type="text" v-model="name" />
</label>
</template>
toRefs 批量解构 reactive
如果我们批量解构 reactive toRef() 就无法胜任了,借用 toRef() 的代码,使用一个 for 循环就实现了 toRefs() :
export function toRefs(object) {
if (!isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
现在来看 toRefs() 的概念是不是好理解了。
toRefs():将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用
toRef()创建的。
扩展 toRefs 的功能
支持 ref
我们知道当 ref API 接收一个复杂类型,内部自动调用 reactive 创建响应式。
<script setup>
import { ref, toRefs } from "vue";
const refObj = ref({
name: "VueUse",
age: 18
});
console.log(refObj.value);
</script>
现在你的链式打点又多了一层 .value,为了支持 ref 复杂数据的提取 toRefs 需要简单的扩展对 ref 复杂数据的支持,只需要增加一个判断就行了。
export function toRefs(maybeRefobject) {
const object = isRef(maybeRefobject) ? maybeRefobject.value : maybeRefobject;
if (!isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret = Array.isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}