33. vue3 中reactivity API

57 阅读8分钟

reactivity api: v3.vuejs.org/api/reactiv…

获取响应式数据

API传入返回备注
reactiveplain-object对象代理深度代理对象中的所有成员
readonlyplain-object or proxy对象代理只能读取代理对象中的成员,不可修改
refany{ value: ... }对value的访问是响应式的
如果给value的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computedfunction{ value: ... }当读取value值时,
根据情况决定是否要运行函数

应用:

  • 如果想要让一个对象变为响应式数据,可以使用reactiveref
  • 如果想要让一个对象的所有属性只读,使用readonly
  • 如果想要让一个非对象数据变为响应式数据,使用ref
  • 如果想要根据已知的响应式数据得到一个新的响应式数据,使用computed

重点理解:

ref 的处理思路。

  1. 如果 ref 传入的是基本数据类型,那么形成的真实结构应该是下面这样的
const count =0;
const countRef = ref(count);

上面代码形成的ref结构应该是 {value:count} ,然后使用proxy来代理这个对象。

  1. 如果是 ref传入的一个普通对象类型,那么我们使用reactive来处理一下,然后形成 {value:objRef} 类型

  2. 如果已经经过了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);

image.png

笔试题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

微任务的优先级高于宏任务

输出结果是:

image.png

判断

API含义
isProxy判断某个数据是否是由reactivereadonly
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的作用 image.png

降低心智负担

所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactivereadonly直接产生的数据

其实上面的意思就是说如果是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 里面下面也是函数 image.png
  • computer 里面下面也是函数

1.我们常用的被监控的函数主要有

  • render
  • computed 回调
  • watchEffect
  • watch

image.png

2.函数运行期间用到了响应式数据

  • ref
  • reactive

3.响应式数据变化会导致函数重新运行

父组件的代码 image.png

子组件代码 image.png

1. count 数据变化,doubleCount的数据会变化吗?

image.png 不会变化,这个是数据和数据产生关联,不符合响应式是数据和函数之间的关联,不符合。 另外doubled 是和render函数产生关联的,手动修改doubled,数据是会改变的。

2. count 数据变化,doubleCount的数据会变化吗?

image.png

会变化,注意watchEffect一开始会运行一次。

3. count 数据变化,doubleCount的数据会变化吗?

image.png 不会,因为传入方法的count已经不是响应式数据了。

4. count 数据变化,doubleCount的数据会变化吗?

image.png

5. count 数据变化,doubleCount的数据会变化吗?

image.png

不会,因为传入函数的count已经不是响应式数据了。