Vue3 Reactivity API

Vue3 Reactivity API

Reactivity API 主要用于完成数据的响应式工作。

vue3 - reactivity api

1.获取响应式数据

1.1 相关 API 的基本介绍

API 传入 返回 备注
reactive 普通 object 对象代理 深度代理对象中的所有成员
readonly 普通 object 或 proxy 对象 对象代理 只能读取代理对象中的成员,不可修改
ref 任何数据类型 { value: … } 对 value 的访问是响应式的
如果给 value 的值是一个对象,
则会通过reactive函数进行代理
如果已经是代理,则直接使用代理
computed function 类型 { value: … }` computed 具有缓存机制
当读取 value 值时,
若该 computed 函数的依赖没有发生改变就不运行函数
若依赖发生改变就运行函数并再次缓存 value 值

总结

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

reactivity api使用示例:

import {
    
     reactive, readonly, ref, computed } from "vue";

const state = reactive({
    
    
  name: "forwardXX",
  sex: "男",
  love: "coding",
});

const sayHi = computed(() => {
    
    
  console.log("sayHi changed");
  return `我叫${
      
      state.name}, 性别${
      
      state.sex},爱好${
      
      state.love}`;
});

console.log("state ready"); //state ready
console.log(sayHi.value); //sayHi changed {name: "forwardXX", sex: "男", love: "coding"}
console.log(sayHi.value); //{name: "forwardXX", sex: "男", love: "coding"}

const imState = readonly(state); //imState可读不可写
console.log(imState === state); //false

const stateRef = ref(state); //stateRef.value 是state的proxy对象 可读可写
console.log(stateRef.value === state); //true
state.name = "XX";
state.love = "game";

console.log(imState.name, imState.love); //XX game
console.log(sayHi.value); //sayHi changed {name: "XX", sex: "男", love: "game"}
console.log(sayHi.value); //{name: "XX", sex: "男", love: "game"} computed缓存机制

const imState2 = readonly(stateRef); //imState2可读不可写
console.log(imState2.value === stateRef.value); //false

1.2 相关测试题

测试题 1:按照下面的要求完成函数

function useUser() {
    
    
  // 在这里补全函数
  return {
    
    
    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
  };
}

测试题 2:按照下面的要求完成函数

function useDebounce(obj, duration) {
    
    
  return {
    
    
    value, // 这里是一个只读对象,响应式数据,默认值为参数值
    setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要在duration的时间中防抖
  };
}

1.3 测试题答案

  • 测试题 1 答案:
import {
    
     reactive, readonly } from "vue";
function useUser() {
    
    
  const userOrigin = reactive({
    
    }); //初始默认数据

  const user = readonly(userOrigin); //可读不可写

  function setUserName(newName) {
    
    
    userOrigin.name = newName;
  }

  function setUserAge(newAge) {
    
    
    userOrigin.age = newAge;
  }

  return {
    
    
    user, // 这是一个只读的用户对象,响应式数据,默认为一个空对象
    setUserName, // 这是一个函数,传入用户姓名,用于修改用户的名称
    setUserAge, // 这是一个函数,传入用户年龄,用户修改用户的年龄
  };
}

export default useUser;
  • 测试题 2 答案:
import {
    
     reactive, readonly } from "vue";
function useDebounce(obj, duration) {
    
    
  const originVal = reactive({
    
    
    obj,
    duration,
  });

  const value = readonly(originVal);

  //防抖函数
  const debounce = (fun, delay) => {
    
    
    let timer = null;
    return (...args) => {
    
    
      if (timer) {
    
    
        clearTimeout(timer);
      }
      timer = setTimeout(() => {
    
    
        fun(args);
        timer = null;
      }, delay);
    };
  };

  //增加防抖
  const setValue = debounce((newObj) => {
    
    
    Object.entries(newObj).forEach(([key, val]) => {
    
    
      originVal[key] = val;
    });
  }, value.duration);

  return {
    
    
    value, // 这里是一个只读对象,响应式数据,默认值为参数值
    // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,
    // 混合操作需要在duration的时间中防抖
    setValue,
  };
}

export default useDebounce;

2.监听数据变化

vue3 中主要有watchEffectwatch这两 API 可监听数据变化.

2.1 watchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行

const stop = watchEffect(() => {
    
    
  // 该函数会立即执行,然后自动收集函数中用到的响应式数据,这些响应式数据变化后会再次执行该函数
});
// 通过调用stop函数,会停止监听
stop(); // 停止监听

watchEffect 详细介绍

2.2 watch

类似于于 vue2 的$watch,监听某个数据的变化。

但 vue3 中的 watch 可侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

下面代码可见:

  • 如果是监听reactive格式的响应式数据时,该 API 会放回一个 proxy 对象
    • 如果直接传 state.count 当作侦听器的,就相当于把 state.count 的值 0 传给了监听器,该侦听器也就无法收集依赖,从而无法监听到 count。
    • 所以我们需要借助函数形式,帮助侦听器收集依赖,从而监听到 count
  • 如果是监听ref格式的响应式数据时,该 API 会放回一个对象
    • 我们可以直接将返回的对象 countRef 作为监听器的,此时并没有展开对应的属性值,所以侦听器可以正常监听。
// 1. 监听单个数据的变化
// reactive API 借助函数的形式,帮助watch收集依赖,从而监听 count
const state = reactive({
    
     count: 0 });
watch(
  () => state.count,
  (newValue, oldValue) => {
    
    
    // ...
  },
  options
);
// ref API,直接传放回的对象
const countRef = ref(0);
watch(
  countRef,
  (newValue, oldValue) => {
    
    
    // ...
  },
  options
);

// 2. 监听多个数据的变化
watch([() => state.count, countRef], ([new1, new2], [old1, old2]) => {
    
    
  // ...
});

2.3 watchEffect 与 watch 的总结

相同点:

  • 无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列)。

不同点:

watch watchEffect
显式指定依赖数据,依赖数据更新时执行回调函数 自动收集依赖数据,依赖数据更新时重新执行自身
惰性执行,第一次页面展示时不执行,当数据变化时才执行 立即执行,没有惰性,页面的首次加载就会执行
可得到原值与变化后的值 只可得到变化后的值,无法得到原值
既要指明监视的属性,也要指明监视的回调 不用指明监视哪个属性,监视的回调中用到哪个属性就监视哪个属性

因为watchEffect会自动收集依赖,使用卡里更方便,所以开发时一般都选择watchEffect,除非遇到下面的场景:

  • 不希望回调函数一开始就执行
  • 数据改变时,需要参考旧值
  • 需要监控一些回调函数中不会用到的数据

2.3 相关测试题

下面的代码输出结果是什么?

import {
    
     reactive, watchEffect, watch } from "vue";
const state = reactive({
    
    
  count: 0,
});

watchEffect(() => {
    
    
  console.log("watchEffect", state.count);
  //1.watchEffect 0
  //4.watchEffect 2
  //7.watchEffect 4
});

watch(
  () => state.count,
  (count, oldCount) => {
    
    
    console.log("watch", count, oldCount);
    //5.watch 2 0
    //8.watch 4 2
  }
);

console.log("start"); //2.start
setTimeout(() => {
    
    
  console.log("time out"); //6.time out
  state.count++;
  state.count++;
});
state.count++;
state.count++;
console.log("end"); //3.end

2.4 测试题答案

这题涉及到了浏览器的事件循环机制、宏任务队列与微任务队列,不懂的朋友参考浏览器事件循环原理.

答案如下:

import {
    
     reactive, watchEffect, watch } from "vue";
const state = reactive({
    
    
  count: 0,
});

watchEffect(() => {
    
    
  console.log("watchEffect", state.count);
  //1.watchEffect 0 立即执行
  //4.watchEffect 2 第一次依赖变化
  //7.watchEffect 4 第二次依赖变化
});
watch(
  () => state.count,
  (count, oldCount) => {
    
    
    console.log("watch", count, oldCount);
    //5.watch 2 0 第一次依赖变化
    //8.watch 4 2 第二次依赖变化
  }
);

console.log("start"); //2.start

//宏任务
setTimeout(() => {
    
    
  console.log("time out"); //6.time out
  state.count++;
  state.count++; //依赖变化 第二次触发watch watchEffect执行 微任务
});

state.count++;
state.count++; //依赖变化 第一次触发watch watchEffect执行 微任务
console.log("end"); //3.end

3.判断响应式格式 API

API 含义
isProxy 判断某个数据是否是由reactivereadonly
isReactive 判断某个数据是否是通过reactive创建的。
isReadonly 判断某个数据是否是通过readonly创建的
isRef 判断某个数据是否是一个ref对象

详细介绍

4.转换响应式格式 API

vue3 转换数据响应式格式相关的 API 有:unref、toRef、toRefs

4.1 unref

如果参数是 ref,则返回内部值,否则返回参数本身。
unref 十一和语法糖,相当于:isRef(val) ? val.value : val

function useNewTodo(todos) {
    
    
  todos = unref(todos);
  // ...
}

4.2 toRef

基于一个响应式对象上的某个属性,创建一个对应的 ref。

这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,改变 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

4.3 toRefs

将一个响应式对象的所有属性均使用 toRef()转换为 ref 格式,然后再将这些属性包装到一个普通对象中,并将该对象返回。

const state = reactive({
    
    
  foo: 1,
  bar: 2,
});

const stateAsRefs = toRefs(state);
/*
stateAsRefs: not a proxy
{
  foo: { value: ... },
  bar: { value: ... }
}
*/

4.4 使用场景:

如果reactive格式的响应式数据直接使用...展开式运算符展开或使用{ }解构运算符解构,会使得到的新属性值失去响应式,所以此时应使用toRefs,再次使其变成响应式数据。

  1. 扩展运算符案例:
//展开运算符
//未使用toRefs
setup(){
    
    
  const state1 = reactive({
    
    a:1, b:2});
  const state2 = reactive({
    
    c:3, d:4});
  return {
    
    
    ...state1, // lost reactivity 失去了响应式
    ...state2 // lost reactivity
  }
}
//使用toRefs
setup(){
    
    
  const state1 = reactive({
    
    a:1, b:2});
  const state2 = reactive({
    
    c:3, d:4});
  return {
    
    
    ...toRefs(state1), // reactivity
    ...toRefs(state2) // reactivity
  }
}
  1. 解构运算符案例:
// 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
}

5.实际开发场景

在我们实际开发时,为了减轻我们的负担,省去判断响应式格式等多余工作。

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

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()
  }
}

结语

这是我目前所了解的知识面中最好的解答,当然也有可能存在一定的误区。

所以如果对本文存在疑惑,可以在评论区留言,我会及时回复的,欢迎大家指出文中的错误观点。

最后码字不易,觉得有帮助的朋友点赞、收藏、关注走一波。

猜你喜欢

转载自blog.csdn.net/forward_xx/article/details/127330385