「全网最详细」Vue3中ref和reactive的区别,很多人还搞不明白!

在Vue3中,我们可以用 refreactive 来定义响应式数据,但这两者有什么区别,什么情况下用 ref,什么情况下用 reactive 呢?

这篇文章我将给大家详细的讲讲 ref 和 reactive 两者的用法以及使用场景。

ref的使用

作用

ref 的作用是将一个普通的 JavaScript 变量(该变量可以是基本类型的数据,也可以是引用类型的数据)包装成一个响应式的数据。

官方文档的解释是:接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。

参数

ref 的参数可以是:基本数据类型、引用数据类型、DOM的ref属性值

基本用法

首先,先看看 ref 函数传入的参数为原始数据类型的情况:

原始数据类型共有7个,分别是:String / Number / Boolean / BigInt / Symbol / Null / Undefined

typescript 复制代码
<script setup>
  import { ref } from 'vue'

  const count = ref(0)
  const handleCountIncrement = () => {
    count.value++
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="handleCountIncrement">count++</button>
  </div>
</template>

上面这段代码中

  • 首先,我们导入了 vue 提供的 ref 函数
  • 然后,使用 ref 函数创建了一个名为 count 的响应式引用,初始值为原始数据类型 0
  • 接着,我们定义了一个方法 handleCountIncrement,该方法用于更新 count 的值,当我们点击按钮更新count时,界面的UI也会发生变化。

ref 函数传入的参数为引用数据类型的情况:

typescript 复制代码
<script setup>
  import { ref } from 'vue'
  const product = ref({ price: 0 })
  
  const changeProductPrice = () => {
    product.value.price += 10
  }
</script>

<template>
  <div class="main">
    <p>price: {{ product.price }}</p>
    <button @click="changeProductPrice">修改产品价格</button>
  </div>
</template>

上面这段代码中

  • 首先,我们导入了 vue 提供的 ref 函数
  • 然后,使用 ref 函数创建了一个名为 product 的响应式引用,初始值为引用数据类型,是一个对象
  • 接着,我们定义了一个方法 changeProductPrice,该方法用于更新 product 对象中 price 的值,当我们点击按钮时,界面的UI也会发生变化。

从上面的代码中,我们可以看到:

ref 函数的参数,我们可以传递原始数据类型的值,也可以传递引用类型的值,但是需要注意的是:

  • 如果传递的是原始数据类型的值,那么指向原始数据的那个值保存在返回的响应式数据的 .value 中,例如上面的 count.value;
  • 如果传递的一个引用类型的值,例如传个对象,返回的响应式数据的 .value 中对应有指向原始数据的属性,例如上面的 product.value.price。

我们不妨打印一下 count 和 product 这两个响应式数据,看看有什么不一样的地方:

上图中,我们可以看到:不管给 ref 函数传递原始数据类型的值还是引用数据类型的值,返回的都是由 RefImpl 类构造出来的对象,但不同的是对象里面的 value:

  • 如果 ref 函数参数传递的是原始数据类型的值,那么 value 是一个原始值
  • 如果 ref 函数参数传递的是引用数据类型的值,那么 value 是一个 Proxy 对象

ref的解包

ref在模板中的解包

  • 在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
  • 如果文本插值表达式( {{ }} )计算的最终值是 ref ,那么也会被自动解包。

在下面的例子中,count 和 person 是顶级属性,但 person. age 不是

typescript 复制代码
<script setup>
  import { ref } from 'vue'

  const count = ref(0)
  const person = {
    age: ref(26)
  }
</script>

那么下面这种写法会自动解包:

typescript 复制代码
<template>
  <div class="main">
    <p>count: {{ count + 1 }}</p>
  </div>
</template>

但下面这种写法不会自动解包:

typescript 复制代码
<template>
  <div class="main">
    <p>age: {{ person.age + 1 }}</p>
  </div>
</template>

页面渲染的结果是:[object Object]1,因为在计算表达式时 person.age 没有被解包,仍然是一个 ref 对象。

为了解决这个问题,我们可以将 age 解构成为一个顶级属性:

typescript 复制代码
<script setup>
  import { ref } from 'vue'

  const person = {
    age: ref(26)
  }
  const { age } = person
</script>

<template>
  <div class="main">
    <p>age: {{ age + 1 }}</p>
  </div>
</template>

现在页面就可以渲染出正确的结果了:age: 27

ref在响应式对象中的解包

一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。

换句话说,它的行为就像一个普通的属性:

typescript 复制代码
<script setup>
  import { ref, reactive } from 'vue'

  const count = ref(0)
  const state = reactive({
    count
  })
  
  console.log(state.count) // 0
  
  state.count = 1
  console.log(count.value) // 1
</script>

如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:

typescript 复制代码
<script setup>
  import { ref, reactive } from 'vue'

  const count = ref(0)
  const state = reactive({
    count
  })
  const otherCount = ref(2)
  
  state.count = otherCount
  console.log(state.count) // 2
  // 原始 ref 现在已经和 state.count 失去联系
  console.log(count.value) // 0
</script>

ref在数组合原生集合类型中的解包

与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型(如 Map) 中的元素被访问时,它不会被解包:

typescript 复制代码
<script setup>
  import { ref, reactive } from 'vue'

  const books = reactive([ref('Vue 3 Guide')])
  // 这里需要 .value
  console.log(books[0].value) // Vue 3 Guide
  
  const map = reactive(new Map([['count', ref(0)]]))
  // 这里需要 .value
  console.log(map.get('count').value) // 0
</script>

总结

  • ref 函数接受的参数数据类型可以是原始数据类型也可以是引用数据类型。
  • 在模板中使用 ref 时,我们不需要加 .value,因为当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,但在js中,访问和更新数据都需要加 .value。

reactive的使用

作用

reactive 的作用是将一个普通的对象转换成响应式对象。它会递归地将对象的所有属性转换为响应式数据。它返回的是一个 Proxy 对象。

参数

reactive 的参数只能是对象或者数组或者像 Map、Set 这样的集合类型。

基本用法

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  // 使用 reactive 创建一个包含多个响应式属性的对象
  const person = reactive({
    name: 'Echo',
    age: 25,
  })
  
  console.log(person.name); // 读取属性值:'Echo'
  person.age = 28;          // 修改属性值
  console.log(person.age);  // 读取修改后的属性值:28

</script>

上面这段代码中

  • 首先,我们导入了 vue 提供的 reactive 函数
  • 然后,使用 reactive 函数创建了一个名为 person 的响应式对象,对象中有 name 和 age 属性
  • 接着,我们读取和修改对象中的属性值。

下面我们在控制台中打印一下 person 对象,看是什么东西:

typescript 复制代码
console.log(person);

可以看到,打印出来的是一个 Proxy 对象,也就是说:reactive 实现响应式就是基于ES6 Proxy 实现的。

那么,Proxy 有几个特点是需要我们注意的:

reactive() 返回的是一个原始对象的 Proxy,它和原始的对象是不相等的。

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  const raw = {}
  const proxy = reactive(raw)
  
  console.log(proxy === raw) // false

</script>

当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化。

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  const obj = {
    count: 1
  }
  const proxy = reactive(obj);
  
  proxy.count++;
  console.log(proxy.count); // 2
  console.log(obj.count);   // 2
</script>

上面这段代码中:

  • 我们定义了一个原始对象 obj 和一个代理对象 proxy
  • 我们更改代理对象中的 count 值,让它自增1
  • 打印原始对象和代理对象的 count,值都是 2
  • 说明:代理对象里面的数据发生变化时,对应的原始数据也会发生变化。

我们再看看另外一种情况,将上面代码中的 proxy.count++ 改为 obj.count++。

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  const obj = {
    count: 1
  }
  const proxy = reactive(obj);
  
  obj.count++;
  console.log(proxy.count); // 2
  console.log(obj.count);   // 2
</script>

控制台打印的结果也都是 2,说明:当原始对象里面的数据发生改变时,会影响代理对象。

那么问题来了,当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化,这是必然的,但是我们实际开发中应该操作原始对象还是代码对象?

答案是:代理对象,因为代理对象是响应式的。

官方给出的建议也是如此:只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  const obj = {
    count: 1
  }
  const proxy = reactive(obj);
</script>

<template>
  <div class="main">
    obj.count:<input type="text" v-model="obj.count">
    proxy.count:<input type="text" v-model="proxy.count">
    <p>obj.count:{{ obj.count }}</p>
    <p>proxy.count:{{ proxy.count }}</p>
  </div>
</template>

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:

typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  const raw = {}
  const proxy1 = reactive(raw)
  const proxy2 = reactive(raw)
  
  console.log(proxy1 === proxy2) // true
  console.log(reactive(proxy1) === proxy1) // true
</script>

注意

  • 使用 reactive 定义的响应式对象,会深度监听每一层的属性,它会影响到所有嵌套的属性。换句话说:一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。
typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  let obj = reactive({
    name: 'Echo',
    a: {
      b: {
        c: 1
      }
    }
  })

  console.log("obj: ", obj)
  console.log("obj.name: ", obj.name)
  console.log("obj.a: ", obj.a)
  console.log("obj.a.b: ", obj.a.b)
  console.log("obj.a.b.c: ",obj.a.b.c)
</script>

控制台打印的结果:

我们可以看到,返回的对象以及其中嵌套的对象都会通过 Proxy 包裹。

若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,我们可以使用 shallowReactive()。

typescript 复制代码
<script setup>
  import { shallowReactive } from 'vue'

  let obj = shallowReactive({
    name: 'Echo',
    a: {
      b: {
        c: 1
      }
    }
  })

  console.log("obj: ", obj)
  console.log("obj.name: ", obj.name)
  console.log("obj.a: ", obj.a)
  console.log("obj.a.b: ", obj.a.b)
  console.log("obj.a.b.c: ",obj.a.b.c)
</script>

我们可以看到,只有顶层对象会通过 Proxy 包裹,其余嵌套的对象都没有,因此,只有对象自身的属性是响应式的,下层嵌套的属性都不具有响应式。

typescript 复制代码
<script setup>
  import { shallowReactive } from 'vue'

  let obj = shallowReactive({
    name: 'Echo',
    a: {
      b: {
        c: 1
      }
    }
  })
</script>

<template>
  <div class="main">
    obj.name: <input type="text" v-model="obj.name">
    obj.a.b.c: <input type="text" v-model="obj.a.b.c">

    <p>obj.name: {{ obj.name }}</p>
    <p>obj.a.b.c: {{ obj.a.b.c }}</p>
  </div>
</template>
  • reactive 的参数只能是对象或者数组或者像 Map、Set 这样的集合类型。如果是原始数据类型,控制台会报警告。
typescript 复制代码
<script setup>
  import { reactive } from 'vue'

  let count = reactive(0)
</script>
  • 当我们将响应式对象的原始类型属性进行解构时,或者将该属性传递给函数时,会丢失响应式。
scss 复制代码
const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)

ref 和 reactive 的区别

经过上面对 ref 和 reactive 的深入了解,我们可以知道它们两者之间的区别了:

  • ref 主要用于创建单个的响应式数据。reactive 用于创建包含多个响应式属性的对象。
  • 对于基本类型(例如:数字,布尔值)的变量定义,推荐使用 ref,如果需要响应式包装对象或数组,推荐使用 reactive。
  • 在模板中使用响应式数据时,无需使用 .value 访问 ref 类型的数据,而是直接使用变量名,而使用 reactive 类型的数据,则直接使用对象属性名。
  • reactive 会递归地将对象的所有属性转换为响应式数据。
  • ref 返回一个由 RefImpl 类构造出来的对象,而 reactive 返回一个原始对象的响应式代理 Proxy。

还有一种区别,使用 watch 侦听 ref 和 reactive 的方式是不同的,下面详细讲一下有什么不一样的地方。

1. 使用 watch 侦听 ref 定义的响应式数据(参数是原始数据类型的情况)

typescript 复制代码
<script setup>
  import { ref, watch } from 'vue'

  let count = ref(0)
  watch(count, (newValue, oldValue) => {
    console.log(`count的值变化了,新值:${newValue},旧值:${oldValue}`)
  })
  const changeCount = () => {
    count.value += 10;
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="changeCount">更新count</button>
  </div>
</template>

上面这段代码中:

  • 我们使用 ref 定义了一个响应式数据 count,初始值是原始数据类型(数字类型)0。
  • 然后使用 watch 函数侦听 count 值的变化。
  • 当我们点击按钮"更新count"时,可以看到控制台会打印输出

也就是说:当侦听的数据是用 ref 定义的原数类型的数据时,数据发生变化的时候,就会执行 watch 函数的回调。

2. 使用 watch 侦听 ref 定义的响应式数据(参数是引用数据类型的情况)

typescript 复制代码
<script setup>
  import { ref, watch } from 'vue'

  let count = ref({ num: 0 })
  watch(count, () => {
    console.log(`count的值发生变化了`)
  })
  const changeCount = () => {
    count.value.num += 10;
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="changeCount">更新count</button>
  </div>
</template>

上面这段代码中:

  • 我们使用 ref 定义了一个响应式数据 count,初始值是引用数据类型,是一个对象。
  • 然后使用 watch 函数侦听 count 值的变化。
  • 当我们点击按钮"更新count"时,可以看到界面的 count 值更新了,但控制台并没有打印输出。

这种情况是因为 watch 并没有对 count 进行深度侦听,但是需要注意的是,此时的 DOM 是能够更新的,

要想深度侦听,只需要加一个对应的参数即可,{ deep: true }。

typescript 复制代码
<script setup>
  import { ref, watch } from 'vue'

  let count = ref({ num: 0 })
  watch(
    count,
    () => {
      console.log(`count的值发生变化了`)
    },
    { deep: true }
  )
  const changeCount = () => {
    count.value.num += 10;
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="changeCount">更新count</button>
  </div>
</template>

此时,我们点击按钮,DOM更新了,控制台也打印输出了。

我们对上面的代码再进行改造下,直接侦听 count.value,但是不深度侦听,看看 DOM 有没有更新并且控制台有没有打印输出。

typescript 复制代码
<script setup>
  import { ref, watch } from 'vue'

  let count = ref({ num: 0 })
  watch(count.value, () => {
    console.log(`count的值发生变化了`)
  })
  const changeCount = () => {
    count.value.num += 10;
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="changeCount">更新count</button>
  </div>
</template>

可以看到,DOM更新了,控制台也打印输出了,这是什么原因呢?

我们打印一下 count.value 看看,发现打印出来的结果是一个 Proxy 代理对象。因为对象类型的数据经过 ref 函数加工会变成引用对象,而该对象的 value 是 Proxy 类型的。所以我们如果需要监视 Proxy 对象中的数据则需要监视的是 coutn.value 的结构。

3. 使用 watch 侦听 reactive 定义的响应式数据

typescript 复制代码
<script setup>
  import { reactive, watch } from 'vue'

  let count = reactive({ num: 0 })
  watch(count, () => {
    console.log(`count的值发生变化了`)
  })
  const changeCount = () => {
    count.num += 10;
  }
</script>

<template>
  <div class="main">
    <p>count: {{ count }}</p>
    <button @click="changeCount">更新count</button>
  </div>
</template>

上面这段代码中:

  • 我们使用 reactive 定义了一个响应式数据 count,传入的是一个对象。
  • 然后使用 watch 函数侦听 count 值的变化。
  • 当我们点击按钮"更新count"时,可以看到界面的 count 值更新了,控制台也有打印输出。

从上面代码中我们可以看到,用 watch 函数侦听 reactive 数据时,不需要添加 deep 属性,也能够对其深度侦听。

总结

以上就是我对 Vue3.0 中 ref 和 reactive 函数的了解,希望可以帮助正在学习 Vue3.0 的朋友!

如果有不正确的地方,欢迎大家在评论区多多指正!

看完记得点个赞哦~ 谢谢!🤞🤞🤞

相关推荐
常常不爱学习1 天前
Vue3 + TypeScript学习
开发语言·css·学习·typescript·html
黄毛火烧雪下1 天前
React Native (RN)项目在web、Android和IOS上运行
android·前端·react native
fruge1 天前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj1 天前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户4099322502121 天前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端11 天前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试1 天前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机1 天前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
疯狂踩坑人1 天前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia1 天前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc