reactivity api: v3.vuejs.org/api/reactiv…
获取响应式数据
API | 传入 | 返回 | 备注 |
---|---|---|---|
reactive | plain-object | 对象代理 | 深度代理对象中的所有成员 |
readonly | plain-object or proxy | 对象代理 | 只能读取代理对象中的成员,不可修改 |
ref | any | { value: ... } | 对value的访问是响应式的 如果给value的值是一个对象, 则会通过 reactive 函数进行代理如果已经是代理,则直接使用代理 |
computed | function | { value: ... } | 当读取value值时, 会根据情况决定是否要运行函数 |
应用:
- 如果想要让一个对象变为响应式数据,可以使用
reactive
或ref
- 如果想要让一个对象的所有属性只读,使用
readonly
- 如果想要让一个非对象数据变为响应式数据,使用
ref
- 如果想要根据已知的响应式数据得到一个新的响应式数据,使用
computed
重点理解:
ref
的处理思路。
- 如果
ref
传入的是基本数据类型,那么形成的真实结构应该是下面这样的
const count =0;
const countRef = ref(count);
上面代码形成的ref结构应该是 {value:count}
,然后使用proxy来代理这个对象。
-
如果是
ref
传入的一个普通对象类型,那么我们使用reactive
来处理一下,然后形成{value:objRef}
类型 -
如果已经经过了
reactive
处理后,那么不会再产生新的ref
代理,直接将这个放在{value:objRef}
的objRef
位置。
const state = reactive({
firstName: "Xu Ming",
lastName: "Deng",
address:{
a:1,
b:2
}
});
reactive
深度代理对象中的所有成员,意思是你使用state.address.a
来获取对象的时候是一个响应式的,但是并不代表下面的address也是响应式的。可以肯定的是下面的address肯定不是响应式的。如果想把address也转换成响应式的用toRefs
const address = state.value.address;
笔试题1:下面的代码输出结果是什么?
import { reactive, readonly, ref, computed } from "vue";
const state = reactive({
firstName: "Xu Ming",
lastName: "Deng",
});
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 = "Cheng";
state.lastName = "Ji";
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);
笔试题2:按照下面的要求完成函数
function useUser(){
// 在这里补全函数
return {
user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
}
}
答案: 其实就是先生成一个reactive,然后再用前面生成的和readonly继续生成一个响应式数据, 然后把继续生成的响应式数据暴露出去即可,这样外面就改不了了。
如果想改的话,暴露一个方法通过reactive出来的响应式数据去修改。 其实本质就是在reactive的生成的proxy外面又增加了一层proxy。
function useUser() {
// 在这里补全函数
const userOrigin = reactive({});
const user = readonly(userOrigin);
const setUserName = (name) => {
userOrigin.name = name;
};
const setUserAge = (age) => {
userOrigin.age = age;
};
return {
user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
};
}
笔试题3:按照下面的要求完成函数
function useDebounce(obj, duration){
// 在这里补全函数
return {
value, // 这里是一个只读对象,响应式数据,默认值为参数值
setValue // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
}
}
答案:
function useDebounce(obj, duration) {
// 在这里补全函数
const valueOrigin = reactive(obj);
const value = readonly(valueOrigin);
let timer = null;
const setValue = (newValue) => {
clearTimeout(timer);
timer = setTimeout(() => {
console.log("值改变了");
// 其实就是将newValue里面的东西混合到 原来valueOrigin里面去。
Object.entries(newValue).forEach(([k, v]) => {
valueOrigin[k] = v;
});
}, duration);
};
return {
value, // 这里是一个只读对象,响应式数据,默认值为参数值
setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
};
}
const { value, setValue } = useDebounce({ a: 1, b: 2 }, 5000);
监听数据变化
watchEffect
const stop = watchEffect(() => {
// 该函数会立即执行,然后追中函数中用到的响应式数据,响应式数据变化后会再次执行
})
// 通过调用stop函数,会停止监听
stop(); // 停止监听
watch
// 等效于vue2的$watch
// 监听单个数据的变化
const state = reactive({ count: 0 })
watch(() => state.count, (newValue, oldValue) => {
// ...
}, options)
const countRef = ref(0);
watch(countRef, (newValue, oldValue) => {
// ...
}, options)
// 监听多个数据的变化
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
// ...
});
- 这里注意写法的问题,state.count 这里与你在哪里直接写了个0,这里不是一个对象,此时要写成
() => state.count
样式. - 但是对于
const countRef = ref(0);
,因为countRef已经是一个对象了,所以可以直接写countRef。 - 对于如果你要检测多个数据的情况,你应该把他放在数组里面。
注意:无论是watchEffect
还是watch
,当依赖项变化时,回调函数的运行都是异步的(微队列)
应用:除非遇到下面的场景,否则均建议选择watchEffect
- 不希望回调函数一开始就执行
- 数据改变时,需要参考旧值
- 需要监控一些回调函数中不会用到的数据
笔试题: 下面的代码输出结果是什么?
import { reactive, watchEffect, watch } from "vue";
const state = reactive({
count: 0,
});
watchEffect(() => {
console.log("watchEffect", state.count);
});
watch(
() => state.count,
(count, oldCount) => {
console.log("watch", count, oldCount);
}
);
console.log("start");
setTimeout(() => {
console.log("time out");
state.count++;
state.count++;
});
state.count++;
state.count++;
console.log("end");
宏任务:定时器 微任务:promise回调。watch,watchEffect
微任务的优先级高于宏任务
输出结果是:
判断
API | 含义 |
---|---|
isProxy | 判断某个数据是否是由reactive 或readonly |
isReactive | 判断某个数据是否是通过reactive 创建的详细:v3.vuejs.org/api/basic-r… |
isReadonly | 判断某个数据是否是通过readonly 创建的 |
isRef | 判断某个数据是否是一个ref 对象 |
转换
unref
等同于:isRef(val) ? val.value : val
应用:
function useNewTodo(todos){
todos = unref(todos);
// ...
}
toRef
得到一个响应式对象某个属性的ref格式
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo'); // fooRef: {value: ...}
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs
把一个响应式对象的所有属性转换为ref格式,然后包装到一个plain-object
中返回
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs: not a proxy
{
foo: { value: ... },
bar: { value: ... }
}
*/
应用:
setup(){
const state1 = reactive({a:1, b:2});
const state2 = reactive({c:3, d:4});
return {
...state1, // lost reactivity
...state2 // lost reactivity
}
}
// 对于上面的state1的结构的结果其实就是{a:1, b:2},对proxy结构和普通对象结构是一样的,本质都是取出 a:proxy.a,proxy.a还是等于1,所以proxy结构和普通对象结构是一样的。
setup(){
const state1 = reactive({a:1, b:2});
const state2 = reactive({c:3, d:4});
return {
...toRefs(state1), // reactivity
...toRefs(state2) // reactivity
}
}
// composition function
function usePos(){
const pos = reactive({x:0, y:0});
return pos;
}
setup(){
const {x, y} = usePos(); // lost reactivity
const {x, y} = toRefs(usePos()); // reactivity
}
toRefs的作用
降低心智负担
所有的composition function
均以ref
的结果返回,以保证setup
函数的返回结果中不包含reactive
或readonly
直接产生的数据
其实上面的意思就是说如果是ref产生的数据不处理,不是ref产生的数据都用toRefs去处理一下。
function usePos(){
const pos = reactive({ x:0, y:0 });
return toRefs(pos); // {x: refObj, y: refObj}
}
function useBooks(){
const books = ref([]);
return {
books // books is refObj
}
}
function useLoginUser(){
const user = readonly({
isLogin: false,
loginId: null
});
return toRefs(user); // { isLogin: refObj, loginId: refObj } all ref is readonly
}
setup(){
// 在setup函数中,尽量保证解构、展开出来的所有响应式数据均是ref
return {
...usePos(),
...useBooks(),
...useLoginUser()
}
}
响应式问题终结
响应式描述的是函数和数据的关联,不是数据和数据的关联,数据和数据没法关联。函数必须是被监控的函数,被谁监控呢? vue2是watcher,vue3是effect。
- 模版里面也是render函数和数据的关联
- computer 里面下面也是函数
- computer 里面下面也是函数
1.我们常用的被监控的函数主要有
- render
- computed 回调
- watchEffect
- watch
2.函数运行期间用到了响应式数据
- ref
- reactive
3.响应式数据变化会导致函数重新运行
父组件的代码
子组件代码
1. count 数据变化,doubleCount的数据会变化吗?
不会变化,这个是数据和数据产生关联,不符合响应式是数据和函数之间的关联,不符合。
另外doubled 是和render函数产生关联的,手动修改doubled,数据是会改变的。
2. count 数据变化,doubleCount的数据会变化吗?
会变化,注意watchEffect一开始会运行一次。
3. count 数据变化,doubleCount的数据会变化吗?
不会,因为传入方法的count已经不是响应式数据了。
4. count 数据变化,doubleCount的数据会变化吗?
会
5. count 数据变化,doubleCount的数据会变化吗?
不会,因为传入函数的count已经不是响应式数据了。