Reactivity Api
# Reactivity Api
何为 Reactivity Api?
这是 Vue3 中的 重中之重
他涵盖了 Vue3 中的响应式功能
Vue2中是如何实现数据响应的?
我们是要配置一个 data 对象,这个对象自动的就会变成响应式对吧 这个过程叫 注入
但是 Vue3 不同,它相当于将响应式这个概念抛出来,也就是我们的 reactivity api,我们通过调用 reactivity api 来使我们的数据变为响应式数据。
- reactive
- readonly
- ref
- computed
-- reactive
-作用:深度代理对象中的所有成员,
-类型:对象代理
import { reactive } from "vue";
const obj = { a: 1, b: 2 };
const state = reactive(obj);
这样我们就能得到一个代理对象 state。
如果我们创建一个数值,一个常量。然后通过 reactive 来进行代理,看会发生什么。
import { reactive } from "vue";
const obj = { a: 1, b: 2 };
const count = 1;
const state = reactive(obj);
const state1 = reactive(count);
console.log(state);
console.log(state1);
并不会报错,但是 Vue 给了我们一个警告, value cannot be made reactive,
他说啊 这个类型不能被 reactive 代理。
通过 state 我们可以发现:
state.a 是 1,
state.b 是 2,
state 是一个 Proxy, 是一个代理对象,它代理的使我们的普通对象,由于它是一个 Proxy,所以 如果我们为 state 的成员进行赋值,他是可以收到通知的。
通过上面的例子我们还可以观察到,我们实现一个响应式并没有依赖组件,以前 vue2 的时候我们需要配置 data 对象,但是现在呢 ? 连组件的一根毛都没看见,也就是说现在的 响应式完全是抽离出来,与组件没有任何关系的。
-- readonly
-作用:只能读取代理对象中的成员,但是不能修改
-类型:对象代理(可以传入一个对象或者一个 proxy)
const objOnly = readonly(obj);
objOnly.a = 2;
console.log(objOnly);
我们可以看到控制台上输出了警告,意思是给 a 属性赋值失败,因为目标是一个只读的。
const stateOnly = readonly(state);
state.a = 2;
console.log(stateOnly);
我们看这个例子,通过 readonly 代理 state,然后修改 state 的属性 a。
没有警告,赋值成功对吧,
因为 stateOnly --> 代理的 state --> 代理的 对象 obj
所以如果对象 obj 或者 state 改变 stateOnly 都会跟着改变
-- ref
首先先来说一下为什么会有 ref 这个东西存在,因为我们前面已经看到,reactive,readonly存在,且可以将我们的数据变为响应式了对吧?那为什么还要有 ref 呢?
可以想到的是,随着我们 reactivity api 的抽离,肯定会有一些数据 如一个 number 啊 一个string 字符串啊 这样的数据代理,但是我们上面已经实验过 reactive 不能够对非对象的数据进行代理。 所以 ref 出现了,目的就是解决我们代理 普通数据的需求。
ref 的原理就是创建一个对象,然后把值放到对象的 value 属性里面。
我们学习过 Es 6 知道了 对象中存在访问器 getter 和 setter
{
get value(){
...
},
set value(val){
...
}
}
那么 ref 的实质是什么? 无非就是通过访问器实现,当你读取的时候使用 get 方法,然后当你赋值的时候 运行 set 方法。但是不是盲目的赋值,会判断一下数据的类型。
如果数据是一个 proxy。
那么直接将 value 的值赋值为这个代理,没有必要创建一个代理去代理它对吧
我们可以看下
const stateRef = ref(state);
console.log(stateRef.value === state);
我们看到结果是 true 对吧。
如果数据是普通类型。
普通数据的话,vue 会创建一个对象,然后将这个值赋给 value 属性,然后返回这个代理。
这样就完成了普通数据的响应式。
如果数据是一个对象。
如果是对象的话就不能直接想普通对象一样操作了,因为 赋值给 value 对象后,value 还是一个普通的对象,无法进行响应式 对吧?外面是代理,里面是对象,相当于啥也没干···
所以呢 这个时候,它会调用一下 reactive 将对象变为代理。
-- computed
computed 呢也很常用,它呢接收一个函数,然后根据情况来判断是否会运行
-类型:返回的是一个代理
什么叫根据情况来判断是否会运行呢?
const sum = computed(() => {
console.log("start it");
return state.a + state.b;
});
console.log(sum.value);
console.log(sum.value);
现在呢我们新增一个 computed 让他求和 state 中的 ab 属性,我们看下 computed 的函数会运行几次呢?
答案是1次!
为什么会这样呢?
vue 会收集依赖,同时缓存这个结果,当我第一次运行的时候,会执行函数搜集这个依赖,依赖的 state 中的 a,b 属性。返回我们的代理结果。
当我们第二次运行的时候,vue一看 诶 哥们 你这没有变化呀,那我把之前的结果给你吧!
那么什么时候会再次运行呢? 是不是当我们函数中依赖的响应式数据发生变化才会执行
也就是 state.a 或 state.b 发生变化的时候。
console.log(sum.value);
state.b = 3
console.log(sum.value);
完美运行。
使用 reactive(...) 的到的 是一个 代理对象
const state = reactive({a:1,b:2}) // state 是一个代理
如果使用 ref(...) 得到的是一个 代理对象
const stateRef = ref({a:1,b:2}) // stateRef.value 是一个代理
两种形式
- 如果想让一个对象变为响应式数据,可以使用 reactive 或 ref
- 如果想让一个对象的所有属性只读,可以使用 readonly
- 如果想让一个非对象数据变为响应式数据,可以使用 ref
- 如果要根据已有的响应式数据得到一个新的响应式数据,可以使用 computed
ok 接下来我们联系一下
Problem -1
请问下面 console.log 输出了什么
import { reactive, readonly, ref, computed } from "vue";
const state = reactive({ firstName: "Xiao Ming", lastName: "Wang" });
const fullName = computed(() => {
console.log("changed");
return `${state.lastName},${state.firstName}`;
});
console.log("state ready");
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);
const imState = readonly(state);
console.log(imState === state);
const stateRef = ref(state);
console.log(stateRef.value === state);
state.firstName = "Xiao Hong";
state.lastName = "Li";
console.log(imState.firstName, imState.lastName);
console.log("fullname is", fullName.value);
console.log("fullname is", fullName.value);
const imState2 = readonly(stateRef);
console.log(imState2.value === stateRef.value);
可以自己写出来然后对照一下
Problem - 2
补全 useUser 函数
import { reactive, readonly, ref, computed } from "vue";
function useUser() {
// 在这里补全函数
return {
user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
setUserAge, // 这是一个函数,传入用户年龄,用户修改用忽的年龄
};
}
答案:这里 reactive 或者 ref 都是可以的 注意好 不要自己乱了就行 因为 ref 的 value 属性才是代理。
import { reactive, readonly, ref, computed } from "vue";
function useUser() {
// 在这里补全函数
const userProp = reactive({});
const user = readonly(userProp);
const setUserName = (name) => {
userProp.name = name;
};
const setUserAge = (age) => {
userProp.age = age;
};
return {
user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
setUserAge, // 这是一个函数,传入用户年龄,用户修改用忽的年龄
};
}
let u = useUser();
const { user } = u;
console.log(user);
console.log(user.name);
u.setUserName("Lisa");
console.log(user.name);
Problem - 3
响应式数据防抖
import { reactive, ref, readonly } from "vue";
function useDebounce(obj, duration) {
const origin = reactive(obj);
const value = readonly(origin);
let timer = null;
const setValue = (val) => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
Object.entries(val).forEach(([k, v]) => {
origin[k] = v;
});
console.log(value)
}, duration);
};
return {
value,
setValue,
};
}
# 监听
watchEffect
watchEffect 接收一个函数,函数呢, 首先会执行一次,同时搜集依赖,如果依赖项改变,则会再次执行函数
import { reactive, watchEffect } from "vue"; const stateOrigin = reactive({ a: 1, b: 2 }); const stop = watchEffect(() => { console.log(stateOrigin.a); }); stateOrigin.a++; stop()
这里呢 如果 执行 stop 则会终止 watchEffect
watch
同 Vue2 中的 $watch
首先我们看下 watch 如何去监听数据
import { reactive, watchEffect, watch } from "vue"; const stateOrigin = reactive({ a: 1, b: 2 }); watch(stateOrigin, (newValue, oldValue) => { console.log("new", newValue.a, "old", oldValue.a); }); stateOrigin.a++;
我们看到结果 新旧值是一个样的 这是为什么 因为我们监听的是一个代理对象。那么我们试试去监听它里面的值达到我们想要的效果。
watch(stateOrigin.a, (newValue, oldValue) => { console.log("new", newValue.a, "old", oldValue.a); });
但是当我们将监听改为监听 stateOrigin 的 a 属性时,vue 给了我们一个警告,意思是我们不能监听一个 1,哦 原来 它会现将这个表达式算出来,再改一改,既然它会计算这个表达式,那我们就使用函数返回这个 a 属性
watch( () => stateOrigin.a, (newValue, oldValue) => { console.log("new", newValue, "old", oldValue); } ); stateOrigin.a++;
测试成功!
ok 当然你也可以直接监听一个 ref
import { reactive, watchEffect, watch, ref } from "vue"; const stateOrigin = reactive({ a: 1, b: 2 }); const countRef = ref(0); watch( () => countRef.value, (newValue, oldValue) => { console.log("new", newValue, "old", oldValue); } ); countRef.value++;
注意 这里 我们可以直接监听一个 ref 因为 他是一个对象,可以被监听
watch( countRef, (newValue, oldValue) => { console.log("new", newValue, "old", oldValue); } ); countRef.value++;
同时 watch 是可以监听多个属性的
import { reactive, watchEffect, watch, ref } from "vue"; const stateOrigin = reactive({ a: 1, b: 2 }); const countRef = ref(0); watch( [countRef, stateOrigin], ([newValue1, newValue2], [oldValue1, oldValue2]) => { console.log("new1", newValue1, "old1", oldValue1); console.log("new2", newValue2, "old2", oldValue2); } ); countRef.value++; stateOrigin.a++;
Watch 和 watchEffect 都是 微队列 异步执行的 全部是等到你的 依赖项执行完毕,然后才会执行监听里面 的回调函数。
然后 什么情况下建议使用 watch 呢
- 如果 你不希望监听一上来就执行,使用 watch
- 如果 你希望得到旧值,使用 watch
- 如果要同时监听多个值
否则 都使用 watchEffect
判断:
- isProxy 判断某个数据是否是 reactive 或 readonly
- isReactive 判断某个数据是否是通过 reactive 创建的
- isReadonly 判断某个数据是否是通过 readonly 创建的
- isRef 判断某个数据是否是一个 ref 对象
转换:
unRef 等同于 isRef(ref)? ref.value:value
toRef 得到一个响应式对象某个属性的 ref 格式
const stateOrigin = reactive({ a: 1, b: 2 }); const countRef = ref(0); const aRef = toRef(stateOrigin, "a"); console.log(aRef.value)
就相当于将 reactive 中的属性值拿出来然后创建了一个 ref
toRefs() 将一个对象中的所有成员都变为 ref
setup(){ const state1Origin = reactive({a:1}) const state1Origin = reactive({b:2}) return{ ...state1Origin, // lose reactivity ...state2Origin, // lose reactivity } }
如上,我们在 setup 中 会遇到合并的需求,但是这样一扩展出来 就变成了普通对象,失去了响应式
这个时候我们怎么做呢,我们是不是可以给要扩展的对象用 toRefs包起来 这样,合并之后每个成员都是一个 ref。
setup(){ const state1Origin = reactive({a:1}) const state1Origin = reactive({b:2}) return{ ...toRefs(state1Origin), // lose reactivity ...toRefs(state2Origin), // lose reactivity } }