ref全家桶
# 题目:
这个题目里面有 5 个挑战,我们一个个来看。
# 更新 ref:
<template>
<div>
<p>
<span @click="update(count - 1)">-</span>
{{ count }}
<span @click="update(count + 1)">+</span>
</p>
</div>
</template>
// 挑战 1: 更新 ref
function update(value) {
count.value = value;
}
首先可以看到调用 update 方法的地方是 template 里两个 span 的 click 事件。然后看到 update 这个方法体,这个挑战是要更新这个 ref 的值,所以直接赋值即可。
# 检查 count 是否为一个 ref 对象:
这里我们可以看到 vue 官方文档,API 这个分类下的组合式 API 里的响应式工具,可以看到这里介绍了 isRef 这个工具函数。这个函数可以检查某个值是否为 ref。
import { ref, Ref, reactive, isRef } from "vue";
/**
* 挑战 2: 检查`count`是否为一个 ref 对象
* 确保以下输出为1
*/
console.log(
// impl ? 1 : 0
isRef(count) ? 1 : 0
);
那么我们引入 isRef 这个工具函数之后,直接传入 count,检测为 ref 变量就会返回 true 那么就会打印 1 达到我们要的效果。
# 如果参数是一个 ref,则返回内部值,否则返回参数本身:
同样可以看到 vue 官方文档,API 这个分类下的组合式 API 里的响应式工具,可以看到这里介绍了 unref 这个语法糖。
举个例子: 假设:a 被 ref 代理了,const a = ref(1),那么我去取值就必须加上 a.value 才能取到 1, 如果用了 unref(a), 那么 a 取到的值就是 1。 如果 a 没有被 ref 代理(也没有被 reactive 代理),那么 unref 不会对其产生任何影响,它将直接返回该值,用 unref(a) a 取到还是 1。
简而言之, unref 目的是确保获取到的值是底层非代理的值。
import { ref, Ref, reactive, unref } from "vue";
/**
* 挑战 3: 如果参数是一个 ref,则返回内部值,否则返回参数本身
* 确保以下输出为true
*/
function initialCount(value: number | Ref<number>) {
// 确保以下输出为true
console.log(unref(value) === 10);
}
那么我们引入 unref 这个工具函数之后,直接传入 value,就可以取到这个 value 的值而不是代理的值。
# 为源响应式对象上的某个 property 新创建一个 ref:
同样可以看到 vue 官方文档,API 这个分类下的组合式 API 里的响应式工具,可以看到这里介绍了 toRef 这个语法糖。toRef 函数接受两个参数:第一个参数是源对象,第二个参数是源对象的属性名。
import { ref, Ref, reactive, toRef } from "vue";
/**
* 挑战 4:
* 为源响应式对象上的某个 `property` 新创建一个 `ref`。
* 然后,`ref` 可以被传递,它会保持对其源`property`的响应式连接。
* 确保以下输出为true
*/
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, "foo"); // 修改这里的实现...
// 修改引用将更新原引用
fooRef.value++;
console.log(state.foo === 2);
// 修改原引用也会更新`ref`
state.foo++;
console.log(fooRef.value === 3);
那么我们引入 toRef 这个工具函数之后,直接传入 相应的源对象和属性名就可以达到我们想要的效果了。
# 完整代码:
<script setup lang="ts">
import { ref, Ref, isRef, unref, toRef, reactive } from "vue";
const initial = ref(10);
const count = ref(0);
// 挑战 1: 更新 ref
function update(value) {
// 实现...
count.value = value;
}
/**
* 挑战 2: 检查`count`是否为一个 ref 对象
* 确保以下输出为1
*/
console.log(
// impl ? 1 : 0
isRef(count) ? 1 : 0
);
/**
* 挑战 3: 如果参数是一个 ref,则返回内部值,否则返回参数本身
* 确保以下输出为true
*/
function initialCount(value: number | Ref<number>) {
// 确保以下输出为true
console.log(unref(value) === 10);
}
initialCount(initial);
/**
* 挑战 4:
* 为源响应式对象上的某个 `property` 新创建一个 `ref`。
* 然后,`ref` 可以被传递,它会保持对其源`property`的响应式连接。
* 确保以下输出为true
*/
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, "foo"); // 修改这里的实现...
// 修改引用将更新原引用
fooRef.value++;
console.log(state.foo === 2);
// 修改原引用也会更新`ref`
state.foo++;
console.log(fooRef.value === 3);
</script>
<template>
<div>
<h1>msg</h1>
<p>
<span @click="update(count - 1)">-</span>
{{ count }}
<span @click="update(count + 1)">+</span>
</p>
</div>
</template>
# 思考:
# 在日常开发中,是用 ref 还是用 reactive 呢?
在解释这个问题之前,需要了解 Vue3
响应式系统的基本知识
let price = 20;
let count = 2;
const total = price * count;
console.log(total); // 40
price = 30;
console.log(total); // 期望是60还是40
这里我们假设原生 JavaScript
就是响应式的,那么我们改变了 price
的值后 total
应该也变了,但不幸的是,他不是响应式的。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key);
},
});
}
因此,Vue
框架必须实现一种机制来跟踪局部变量的读和写,它是通过拦截对象属性的读写来实现的。这样一来,Vue
就可以跟踪一个响应式对象的属性访问以及更改。由于浏览器的限制,Vue 2
专门使用 getters/setters
来拦截属性。Vue 3
对响应式对象使用 Proxy
,对 ref
使用 getters/setters
。上面的伪代码展示了属性拦截的基本原理。
# reactive()
让我们使用 Vue3
的 reactive()
函数来声明一个响应式状态:
import { reactive } from "vue";
const state = reactive({ age: 18 });
该状态默认是深度响应式的。如果你修改了嵌套的数组或对象,这些更改都会被 vue 检测到:
import { reactive } from "vue";
const state = reactive({
age: 0,
nested: { count: 0 },
});
watch(state, () => console.log(state));
// "{ age: 0, nested: { count: 0 } }"
const incrementNestedCount = () => {
state.nested.count += 1;
// "{ age: 0, nested: { count: 1 } }"
};
# 限制
reactive()
API 有两个限制:
第一个限制是,它只适用于对象类型,比如对象、数组和集合类型,如 Map
和 Set
。它不适用于原始类型,比如 string
、number
或 boolean
。
第二个限制是,从 reactive()
返回的代理对象与原始对象是不一样的。用===
操作符进行比较会返回 false
:
const plainJsObject = {};
const proxy = reactive(plainJsObject);
console.log(proxy === plainJsObject); // false
你必须始终保持对响应式对象的相同引用,否则,Vue
无法跟踪对象的属性。如果你试图将一个响应式对象的属性解构为局部变量,你可能会遇到这个问题:
const state = reactive({
count: 0,
});
// ⚠️ count现在是一个与state.count断开连接的局部变量
let { count } = state;
count += 1; // ⚠️ 不会影响到原始状态
不过幸运的是,你可以使用 toRefs
将对象的所有属性转换为响应式的,然后你可以解构对象而不丢失响应:
let state = reactive({
count: 0,
});
const { count } = toRefs(state);
# ref()
Vue 提供了 ref()
函数来解决 reactive()
的限制。
ref()
并不局限于对象类型,而是可以容纳任何值类型
import { ref } from "vue";
const count = ref(0);
const state = ref({ count: 0 });
不过为了读写通过 ref()
创建的响应式变量,你需要通过.value
属性来访问:
const count = ref(0);
const state = ref({ count: 0 });
console.log(count); // { value: 0 }
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
state.value.count = 1;
console.log(state.value); // { count: 1 }
看到这里,你可能会有这样的疑问,为什么ref()
能容纳原始类型,因为我们刚刚了解到 Vue
需要一个对象才能触发 get/set
代理陷阱。下面的伪代码展示了 ref()背后的简化逻辑:
function ref(value) {
const refObject = {
get value() {
track(refObject, "value");
return value;
},
set value(newValue) {
value = newValue;
trigger(refObject, "value");
},
};
return refObject;
}
当拥有对象类型时,ref
自动用 reactive()
转换其.value
,所以你也可以理解成,reactive
是基于ref
这个 api 实现的。
ref({}) ~= ref(reactive({}))
感兴趣可以阅读一下源码中 ref (opens new window) 的实现,
不幸的是,ref()
创建的响应式对象也不能进行解构。这也会导致响应式丢失。
很多人在使用 ref
时觉得到处使用.value
可能很麻烦,但我们可以使用一些辅助函数。
unref
实用函数
unref()
是一个便捷的实用函数,在你的值可能是一个 ref
的情况下特别有用。在一个非 ref
上调用.value
会抛出一个运行时错误,unref()
在这种情况下就很有用:
import { ref, unref } from "vue";
const count = ref(0);
const unwrappedCount = unref(count);
如果unref()
的参数是一个ref
,就会返回其内部值。否则就返回参数本身。这是的val = isRef(val) ? val.value : val
语法糖。
那么在模板中,为什么不用.value 来拿值呢? 这是因为当你在模板上调用 ref 时,Vue 会自动使用 unref()进行解包。这样,你永远不需要在模板中使用.value 进行访问:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<span>
<!-- 不需要.value -->
{{ count }}
</span>
</template>
# 总结
让我们总结一下 reactive 和 ref 之间的区别:
reactive | ref |
---|---|
👎 只对对象类型起作用 | 👍 对任何类型起作用 |
👍 在<script> 和<template> 中访问值没有区别 | 👎 访问<script> 和<template> 中的值的行为不同 |
👎 重新赋值一个新的对象会"断开"响应式 | 👍 对象引用可以被重新赋值 |
属性可以在没有.value 的情况下被访问 | 需要使用.value 来访问属性 |
👍 引用可以通过函数进行传递 | |
👎 解构的值不是响应式的 | |
👍 与 Vue2 的 data 对象相似 |
最后说说用 ref 还是 reactive 我个人的观点吧,使用 ref 由于一般都用.value 来访问,那么下意识就会觉得这声明的是一个响应式变量,并且上面也说了 ref 基于 reactive 实现的,所以如果不想麻烦直接用 ref 即可。