Vue 系列之:ref、reactive、toRef、toRefs

前言

setup 函数中默认定义的变量并不是响应式的(即数据变了以后页面不会跟着变),如果想让变量变为响应式的变量,需要使用 refreactive 函数修饰变量。

响应式对象、proxy 对象、ref 对象、reactive 对象,四者之间的关系

  • proxy 对象、ref 对象、reactive 对象都是具体的对象,而响应式对象是一个概念。

    • Proxy 对象是实现响应式对象的一种方式(Vue2 中通过 Object.defineProperty 来实现响应式对象)。

    • ref 对象、reactive 对象都是响应式对象的一种。

  • Proxy 对象是 ES6 中引入的一个新特性,用于创建一个对象的代理,从而可以拦截并自定义对象的基本操作。Proxy 对象可以看作是在目标对象之前架设的一层"拦截",外界对该对象的访问都必须先通过这层拦截。

  • ref 创建的对象本身并不是一个 Proxy 对象,它实际上返回的是一个 RefImpl 类型的对象,是一个包含 .value 属性的响应式对象。

  • 在 Vue3 的响应式系统中,当使用 reactive 函数时,它实际上会返回一个 Proxy 对象。也是就说 reactive 对象其实就是 Proxy 对象。但在 Vue3 的 API 层面,我们通常将其称为"响应式对象"或"reactive 对象",以强调其响应式特性。

ref

ref 函数是 Vue 的响应式 API 之一,用于创建一个响应式的引用对象。它会返回一个响应式的对象,该对象包含一个 value 属性,用于存储和访问实际的值。

html 复制代码
<template>
  <div class="box">
    {{ proxy }}
  </div>
</template>

<script setup>
import { ref } from 'vue';

const proxy = ref('测试')
console.log("test:", proxy);
</script>

<style scoped>
.box {
  padding: 20px;
}
</style>

我们知道 ref 可以生成基本数据类型的响应式数据,那么它可以用来生成引用类型的响应式数据吗?答案是可以的。

js 复制代码
const proxy = ref({ name: '张三', age: 10 })

区别来了:

  • 当给 ref 传递一个原始类型时,它会简单地将这个值包装在一个对象中,这个对象有一个 value 属性指向原始值

  • 当给 ref 传递一个对象或数组这样的复杂类型时,Vue 不仅会创建一个 value 属性来存储该对象,还会用一个 Proxy 对象来包装这个对象,以实现更深层次的响应式。这就意味着当对象更深层次的属性值改变时,这些改变也会被 Vue 自动追踪。

js 复制代码
const proxy = ref({
  name: '张三', age: 10, school: {
    name: '提瓦特小学',
    addr: '蒙德'
  }
})
setTimeout(() => {
  proxy.value.school.teacher = '钟离'
  console.log("test:", proxy);
}, 2000);

通过示例可以看到,给 ref 对象加了一个不存在的属性,也触发了响应式。那为什么很多文章都说 ref 是浅监听呢?它明明实现了深层次的监听呀?

首先搞懂 ref 是怎么实现响应式的:

ref 的基本实现

对于基本数据类型ref 是通过创建一个包含 value 属性的对象来实现响应式的。具体来说,ref 会创建一个带有 getter 和 setter 的对象,这样当 value 被读取或修改时,Vue3 的响应式系统可以检测到这些变化并触发相应的更新。

js 复制代码
function ref(value) {
  const refObject = {
    __v_isRef: true,
    get value() {
      track(refObject, 'get', 'value'); // 跟踪读取操作
      return value;
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue;
        trigger(refObject, 'set', 'value'); // 触发更新操作
      }
    }
  };
  return refObject;
}
  • __v_isRef :这是一个标志属性,用于标识这个对象是一个 ref 对象。

  • get value() :这是一个 getter 方法,当访问 refObject.value 时会调用这个方法。在 getter 中,会调用 track 函数来跟踪读取操作。

  • set value(newValue) :这是一个 setter 方法,当修改 refObject.value 时会调用这个方法。在 setter 中,会检查新值和旧值是否不同,如果不同,则更新值并调用 trigger 函数来触发更新操作。

  • track 函数 :用于跟踪依赖关系。当组件读取 refObject.value 时,track 函数会被调用,记录当前的依赖关系。这样,当 value 发生变化时,Vue 3 可以知道哪些组件需要更新。

  • trigger 函数 :用于触发更新。当 value 发生变化时,trigger 函数会被调用,通知相关的组件进行更新。

对于引用数据类型,ref 会使用 reactive 来创建一个响应式代理对象。reactive 函数又会使用 Proxy 来创建一个响应式代理对象。

js 复制代码
function ref(value) {
  const refObject = {
    __v_isRef: true,
    get value() {
      track(refObject, 'get', 'value'); // 跟踪读取操作
      return value;
    },
    set value(newValue) {
      if (newValue !== value) {
        value = newValue;
        trigger(refObject, 'set', 'value'); // 触发更新操作
      }
    }
  };

  // 如果 value 是一个对象,则使用 reactive 包装
  if (isObject(value)) {
    refObject.value = reactive(value);
  }

  return refObject;
}

function isObject(value) {
  return typeof value === 'object' && value !== null;
}

由于 refObject.value 就是一个响应式对象,所以可以深度监听其内部属性的变化。所以 "ref 是浅监听" 这句话是的!

总结:

  • 对于基本数据类型,ref 是通过创建一个包含 .value 属性的响应式对象来保证响应式的,.value 属性的值就是原来的值

  • 对于引用数据类型,.value 属性的值是通过 reactive 函数来创建的一个响应式对象。

为什么 ref 需要使用 .value?

为什么 ref 在设计的时候要使用 .value,而 reactive 就不需要使用 .value?都不使用 .value 不是更好吗?

这是因为 Vue.js 的响应式系统是基于 Proxy 和 Reflect API 的,而这些 API 只能代理对象,不能代理原始类型。因此 ref 函数会先创建一个 ref 对象,该对象有一个 .value 属性:

  • 对于基本数据类型,ref 会将这个值直接封装在 value 属性中;

  • 对于引用数据类型,ref 也会将这个对象或数组封装在 value 属性中,但 Vue 会使用 Proxy 机制(通过 reactive 函数实现)来确保这个对象或数组是响应式的。

reactive

reactive 函数接受一个对象作为参数,并返回一个新的响应式对象。这个新对象会"代理"原始对象,使得对原始对象的属性进行读取和修改时,Vue 可以追踪这些变化并相应地更新 DOM。

当你使用 reactive 创建一个响应式对象时,Vue 内部实际上是创建了一个 Proxy 对象。

什么是 Proxy 对象

Proxy 是 ES6 提供的一个新特性,它允许我们创建一个对象的代理,从而可以拦截并定义该对象的基本操作(如属性查找、赋值、枚举、函数调用等)的行为。

在 Vue3 中,reactive 函数使用 Proxy 对象来包装原始对象,并拦截对该对象属性的访问和修改操作。当这些操作发生时,Proxy 可以执行一些额外的逻辑,例如依赖收集和触发更新。

底层实现机制

简化源码:

js 复制代码
// 1. reactive 是一个函数
function reactive(target) {
  // 1. 它返回一个 Proxy 对象
  return new Proxy(target, {
    // 2. 在读取属性时(即执行 get 操作)会进行依赖收集
    get(target, key, receiver) {
      // 3. 依赖收集
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      // 如果结果是对象,递归使其成为响应式
      return typeof result === 'object' && result !== null ? reactive(result) : result;
    },
    // 4. 当属性值发生变化时(即执行 set 操作)会触发更新
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        // 5. 触发更新
        trigger(target, key);
      }
      return result;
    }
  });
}

// 假设的 track 和 trigger 函数

// 依赖收集
function track(target, key) {
  /*
  当访问响应式对象的属性时,
  Vue会记录下当前正在执行的副作用函数(effect),
  这些函数依赖于该属性的值。这个过程称为依赖收集。
  */
}

// 触发更新
function trigger(target, key) {
  /*
  当响应式对象的属性发生变化时,
  Vue会通知所有依赖该属性的副作用函数重新执行,
  从而实现视图的自动更新。这个过程称为触发更新。
  */
}

Vue3 的响应式系统通过依赖收集来追踪哪些响应式数据被哪些副作用函数(effect)所依赖。当访问响应式数据时,Vue 会记录下当前正在执行的副作用函数,并建立数据与副作用之间的依赖关系。

当响应式数据发生变化时,Vue 会触发更新操作。这个更新操作会遍历依赖关系图,并逐个执行依赖的副作用函数,从而实现视图的自动更新。

简单来说就是:接收对象--->创建 Proxy--->依赖收集--->触发更新

原始对象与代理对象的关系

通过一个示例来了解他们之间的关系:

js 复制代码
// 原始对象
const obj = {
  name: '张三',
  age: 18
};

// 处理器对象,定义拦截行为
const handler = {
  // 拦截属性读取操作
  get(target, property) {
    console.log(`Getting ${property}`);
    return target[property];
  },

  // 拦截属性设置操作
  set(target, property, value) {
    console.log(`Setting ${property} to ${value}`);
    target[property] = value;
    return true;
  }
};

// 创建代理对象
const proxy = new Proxy(obj, handler);

obj.age = 19
// proxy.age = 20
console.log("obj.age:", obj.age)
console.log("proxy.age:", proxy.age)

可以看到,当原始对象的属性值发生变化时,代理对象的属性值也发生了变化,这是因为代理对象读取的其实还是原始对象的属性值return target[property]

js 复制代码
// obj.age = 19
proxy.age = 20
console.log("obj.age:", obj.age)
console.log("proxy.age:", proxy.age)

可以看到,当代理对象的属性值发生变化时,原始对象的属性值也发生了变化,这是因为改变代理对象的属性值,其实就是在改变原始对象的属性值target[property] = value

问题来了:

当响应式数据发生变化时,Vue 会触发更新操作,这很好理解, 但是当原始对象发生变化时,Vue 也会触发更新操作吗?因为上面的例子中当使用 obj.age = 19 时,并没有触发 set 方法。

来看示例:

html 复制代码
<template>
  <div class="box">
    <div>{{ obj }}</div>
    <div>{{ proxy }}</div>
  </div>
</template>

<script setup>
import { ref, reactive, watch } from 'vue';

// 原始对象
let obj = {
  name: '张三',
  age: 18
};

let proxy = reactive(obj)

setTimeout(() => {
  obj.age = 19
  console.log("obj:", obj);
  console.log("proxy:", proxy);
}, 1000);

watch((proxy), (newVal, oldVal) => {
  console.log("newVal:", newVal);
  console.log("oldVal:", oldVal);
}, { deep: true })

</script>

<style scoped>
.box {
  padding: 20px;
}
</style>


可以看到,当修改原始对象的属性值时,这个修改会反应到 Proxy 对象中(打印的 proxy 值发生了变化),但是 Vue 的响应式系统并没有做出更新(页面中的 age 值没有发生变化,watch 中的内容也没有执行)

所以说:直接操作原始对象会触发 Vue 响应式更新这句话是错的。

在 Vue3 中,如果响应式对象中的某个属性是一个普通对象或数组,并且你在之后将其替换为一个新的对象或数组,那么新的对象或数组将不会是响应式的。

如何理解这段话?看示例:

html 复制代码
<template>
  <div class="box">
    <p>proxy:{{ proxy.user.name }}</p>
    <p>obj:{{ obj.name }}</p>
    <button @click="test">测试</button>
  </div>
</template>

<script setup>
import { ref, reactive, watch } from 'vue';
const proxy = reactive({
  user: { name: '张三' }
});

const obj = { name: '李四' };

function test() {
  proxy.user = obj;
  setTimeout(() => {
    obj.name = '王五';
    console.log("proxy:", proxy);
    console.log("obj:", obj);
  }, 1000);
}
</script>

<style scoped>
.box {
  padding: 20px;
}
</style>


当点击按钮后,页面中显示的是"李四"而不是"王五",但是打印的值都是"王五",说明是视图没有更新,即新对象 obj 不是响应式的。

非响应式转换

将非响应式对象转换成响应式对象可以用 ref 或 reactive,那么将响应式对象转换为非响应式对象有哪些方法呢?

toRaw 函数

该函数的主要作用是获取一个由 reactive 或者 ref 创建的代理对象所包装的原始对象。

html 复制代码
<template>
  <div class="box">
    <p>proxy:{{ proxy.name }}</p>
    <p>obj:{{ obj.name }}</p>
    <button @click="test">测试</button>
  </div>
</template>

<script setup>
import { ref, reactive, toRaw } from 'vue';
const proxy = reactive({
  name: "张三"
});

let obj = toRaw(proxy)

function test() {
  obj.name = '李四';
  console.log("proxy:", proxy);
  console.log("obj:", obj);
}
</script>

<style scoped>
.box {
  padding: 20px;
}
</style>

toRaw 返回的是原始数据的引用。因此,对 rawData 的修改会影响到原始的响应式对象。

点击按钮后,打印值变化了,但是页面值没有变化,丢失了响应性。

直接解构丢失响应性

html 复制代码
<template>
  <div class="box">
    <p>proxy:{{ proxy.name }}</p>
    <p>obj:{{ name }}</p>
    <button @click="test">测试</button>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
const proxy = reactive({
  name: "张三"
});

let { name } = proxy

function test() {
  name = '李四';
  console.log("proxy:", proxy);
  console.log("name:", name);
}
</script>

点击按钮后,页面值没有变化,丢失了响应性。

Vue2 动态添加的新属性

在 Vue2 中,动态添加的新属性默认不是响应式的,这是因为 Vue2 的响应式系统是基于 ES5 的 Object.defineProperty 方法实现的,该方法无法拦截对象属性的添加和删除。

但是 Vue3 使用了 ES6 的 Proxy 代理对象来实现响应式,动态添加的新属性默认是响应式的。

ref 与 reactive 的区别

当你通过 ref 包装一个对象时,你实际上得到的是一个包含 .value 属性的对象,这个 .value 属性指向你原始的对象。

  • 对于基本数据类型,.value 属性的值就是原来的值

  • 对于引用数据类型,.value 属性的值是通过 reactive 函数来创建的一个响应式对象。

reactive 不会包装对象在另一个对象中,而是直接使对象本身变为响应式的。

toRef

toRef 使源响应式对象的某个属性成为一个 ref 对象。它接收两个参数:源响应式对象和属性名,返回一个 ref 数据。

注意点:

  1. "源响应式对象",即源对象需要是 ref 对象或 reactice 对象

  2. "返回一个 ref 数据",即要用 .value 才能获取到实际值

  3. 如果源对象如果是 ref 对象,则要使用 .value;reactive 对象则不需要

  4. 修改返回的 ref 对象时源对象也会跟着变

html 复制代码
<template>
  <div class="box">
    <p>refObj:{{ refObj }}</p>
    <p>reactiveObj:{{ reactiveObj }}</p>
  </div>
</template>

<script setup>
import { ref, reactive, toRef } from 'vue';

const proxy1 = ref({
  name: '张三', age: 10, school: {
    name: '提瓦特小学',
    addr: '蒙德'
  }
})

const proxy2 = reactive({
  name: '张三', age: 10, school: {
    name: '提瓦特小学',
    addr: '蒙德'
  }
})

let refObj = toRef(proxy1.value, 'school') // ref 源对象需要使用 .value
let reactiveObj = toRef(proxy2, 'school')

console.log("refObj:", refObj);
console.log("reactiveObj:", reactiveObj);
</script>

<style scoped>
.box {
  padding: 20px;
}
</style>

在 Vue3 中,toRef 函数的作用是使响应式对象的属性也成为响应式对象,响应式对象的属性本来就是响应式的,为什么还需要用 toRef 函数去转换呢?也就是说 toRef 的实际使用场景有哪些?

  1. 解构响应式对象时保持响应性 :当你解构一个响应式对象时,得到的属性会失去它们的响应性,因为它们不再是响应式对象的一部分。使用 toRef 可以让你在解构后仍然保持这些属性的响应性。

  2. 传递响应式属性作为 props :当你将响应式对象的属性作为 prop 传递给子组件时,如果直接传递属性本身,那么子组件将接收到属性的当前值,而不是一个响应式引用。这意味着如果父组件中的属性值发生变化,子组件将不会接收到这个更新,除非使用了 .sync 修饰符或 v-model。但是,使用 toRef 可以创建一个响应式引用,并将其作为 prop 传递,这样子组件就可以响应父组件中属性值的变化了。

toRefs

toRefs 它接受一个响应式对象作为参数,并返回一个新的普通对象,普通对象的每个属性都是一个 ref 对象。

注意点:

  1. 返回的是一个普通对象,不是响应式对象

  2. 返回的普通对象的每个属性都是 ref 对象

  3. 改变属性的 ref 对象的值,源响应式对象也会跟着变

html 复制代码
<template>
  <div class="box">
    <p>proxy:{{ proxy }}</p>
    <p>toRefsObj:{{ toRefsObj }}</p>
  </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

const proxy = reactive({
  name: '张三', age: 10, school: {
    name: '提瓦特小学',
    addr: '蒙德'
  }
})

let toRefsObj = toRefs(proxy)

console.log("toRefsObj:", toRefsObj);

setTimeout(() => {
  toRefsObj.name.value = '李四'
}, 1000);
</script>

<style scoped>
.box {
  padding: 20px;
}
</style>

1 秒钟后页面值刷新了,表明是响应式的:

toRefsObj 是普通对象:

toRef 和 toRefs 最大的区别就是:

  • toRef:用于将响应式对象中的单个属性 转换为一个独立的响应式引用(ref)。

  • toRefs:用于将响应式对象中的所有属性都转换为独立的响应式引用。

相关推荐
百万蹄蹄向前冲4 分钟前
组建百万前端梦之队-计算机大学生竞赛发展蓝图
前端·vue.js·掘金社区
云隙阳光i17 分钟前
实现手机手势签字功能
前端·javascript·vue.js
imkaifan36 分钟前
vue2升级Vue3--native、对inheritAttrs作用做以解释、声明的prop属性和未声明prop的属性
前端·vue.js·native修饰符·inheritattrs作用·声明的prop属性·未声明prop的属性
小程序设计37 分钟前
【2025】基于springboot+vue的宠物领养管理系统(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·宠物
小程序设计1 小时前
【2025】基于springboot+vue的体育场馆预约管理系统(源码、万字文档、图文修改、调试答疑)
vue.js·spring boot·后端
柠檬树^-^1 小时前
app.config.globalProperties
前端·javascript·vue.js
1024小神1 小时前
vue/react/vite前端项目打包的时候加上时间最简单版本,防止后端扯皮
前端·vue.js·react.js
轻口味3 小时前
Vue.js 与 RESTful API 集成之使用 Axios 请求数据
前端·vue.js·restful
奶糖 肥晨4 小时前
基于 Vue 和 Element Plus 的时间范围控制与数据展示
前端·vue.js·elementui