Code前端首页关于Code前端联系我们

Vue3 reactive到底是个啥?比ref好用吗?新手容易踩哪些坑?

terry 2小时前 阅读数 36 #Vue

最近有不少刚从Vue2转过来或者刚学前端框架的朋友私信我,说被Vue3的响应式API搞懵了——一会儿是ref一会儿是reactive,到底什么时候用哪个?reactive为什么经常赋值后就不响应了?别急,今天就用大白话把这些问题全说透,连代码细节都给你抠明白,看完你不仅能熟练用reactive,连它的底层原理都能大概摸个门儿清。

reactive到底解决了Vue2的什么问题

先别急着学用法,搞懂它的来龙去脉能帮你更轻松地理解特性,用过Vue2的都知道,Vue2的响应式是靠Object.defineProperty实现的,这个东西有两个硬伤: 第一个硬伤是只能监听对象已有属性的读取和修改,如果是新增属性或者删除属性,Vue2根本感知不到,得手动用$set或者$delete才行,比如你在data里定义了一个user对象,只有name和age两个属性,后来通过user.address = '北京'加了地址,页面上绑定的user.address永远不会更新,除非你改成this.$set(this.user, 'address', '北京'),这在处理动态表单、复杂数组的时候特别麻烦。 第二个硬伤是数组的响应式支持不完善,Vue2重写了push、pop、shift、unshift、splice、sort、reverse这7个常用的数组方法,用这些方法修改数组页面会更新,但如果你直接通过索引修改数组,比如this.userList[0] = {name: '张三'},或者直接修改数组的length,比如this.userList.length = 2,页面同样不会动,还是得用$set或者splice。

Vue3的reactive就是为了彻底解决这两个问题诞生的——它用的是ES6的Proxy对象,Proxy相当于给原始对象套了一层“代理外壳”,不管你是读取/修改已有属性、新增/删除属性,还是通过索引改数组、改length,甚至是调用数组的任何方法,Proxy都能第一时间拦截到,然后通知视图更新,完全不需要那些额外的API了。

reactive的核心用法是什么?和普通对象有啥区别?

聊完背景,来看reactive的基本操作,其实很简单,就是把一个普通的JavaScript对象(或者数组、Map、Set这些引用类型)传给reactive函数,它会返回一个响应式的代理对象,你对这个代理对象做的任何合法操作,都会触发响应式更新。

举个最基础的例子:

<template>
  <div>
    <p>姓名:{{ user.name }}</p>
    <p>年龄:{{ user.age }}</p>
    <p>地址:{{ user.address }}</p>
    <button @click="updateUserInfo">更新信息</button>
    <button @click="addHobby">添加爱好</button>
    <ul>
      <li v-for="(hobby, index) in user.hobbies" :key="index">{{ hobby }}</li>
    </ul>
    <button @click="updateFirstHobby">修改第一个爱好</button>
  </div>
</template>
<script setup>
import { reactive } from 'vue'
// 把普通对象传给reactive,得到响应式代理
const user = reactive({
  name: '李四',
  age: 25,
  hobbies: ['游泳', '爬山']
})
const updateUserInfo = () => {
  // 修改已有属性,没问题
  user.age++
  // 直接新增属性,没问题,不需要$set!
  user.address = '上海浦东新区'
}
const addHobby = () => {
  // 调用数组方法,没问题
  user.hobbies.push('骑行')
}
const updateFirstHobby = () => {
  // 直接通过索引修改数组,没问题!
  user.hobbies[0] = '滑雪'
}
</script>

你看,这个例子里的操作在Vue2里大部分都得额外处理,但用reactive就直接写就行,非常爽,那这个响应式代理对象和普通对象有啥区别呢?表面上看完全一样,你可以像用普通对象那样访问它的属性、调用它的方法(比如数组的push),但实际上它是Proxy的实例,你可以用console.log(user)或者Object.prototype.toString.call(user)看一下,会发现和普通对象的输出不一样。

这里要特别提一句:reactive只能处理引用类型的数据,比如对象、数组、Map、Set这些,不能处理基本类型(字符串、数字、布尔值、null、undefined、Symbol),如果给reactive传一个基本类型,它会直接返回那个基本类型本身,不会有任何响应式效果。

const count = reactive(10) // 没用,count还是普通的10
count++ // 页面不会更新

那基本类型的响应式怎么办?用ref!这就引出了大家最关心的第二个问题:reactive和ref到底选哪个?

reactive和ref怎么选?别再乱选了

很多新手刚接触这两个API的时候,会觉得“既然reactive能处理引用类型,那我把所有数据都包在一个对象里用reactive不就行了?”或者“ref既能处理基本类型又能处理引用类型(后面会说),那我全用ref好了!”其实这两种想法都不对,得根据具体的场景来选。

先来说说ref的隐藏功能

很多人可能只知道ref用来处理基本类型,但其实ref也能处理引用类型,当你给ref传一个引用类型的时候,它内部会自动调用reactive把这个引用类型变成响应式的,然后把这个响应式代理对象挂在ref返回的对象的.value属性上。

import { ref } from 'vue'
// 给ref传对象,内部自动调用reactive
const userRef = ref({
  name: '王五',
  age: 28
})
console.log(userRef.value) // 这是一个reactive生成的响应式代理对象

这时候你要修改userRef里的属性,得通过userRef.value来访问,比如userRef.value.age++,不过在template里不需要加.value,Vue会自动帮你解包。

再讲什么时候用reactive,什么时候用ref

现在知道了两个API的功能,那到底怎么选呢?给大家几个实用的判断标准:

  1. 如果你有一组逻辑相关的属性,优先用reactive:比如用户信息(name、age、address)、表单数据(username、password、email)、列表数据(list、page、pageSize)这些,把它们放在一个对象里用reactive,代码会更清晰,不需要给每个属性都加.value,修改起来也更方便,比如你想重置整个表单,直接把form对象的属性全清空就行,不用一个个改formRef.value.xxx。
  2. 如果你只有一个单独的变量,或者是逻辑不相关的多个变量,优先用ref:比如一个计数器count、一个开关isShow、一个标题title这些,单独用ref更简洁,不需要为了它们特意包一个对象。
  3. 如果你需要把响应式数据解构出来传给子组件,或者在函数里单独使用某个属性,要注意reactive的解构问题(后面会讲这个大坑),这时候可以考虑用ref,或者用toRefs/toRef来处理reactive的解构:这个是新手最容易踩的坑之一,后面会详细说。

比如举个场景对比:

场景1:登录表单

这时候用reactive更合适,因为所有属性都是逻辑相关的:

<template>
  <form @submit.prevent="handleLogin">
    <input type="text" v-model="form.username" placeholder="用户名">
    <input type="password" v-model="form.password" placeholder="密码">
    <button type="submit">登录</button>
  </form>
</template>
<script setup>
import { reactive } from 'vue'
// 逻辑相关的属性放一个reactive对象
const form = reactive({
  username: '',
  password: ''
})
const handleLogin = () => {
  console.log('登录信息:', form)
  // 重置表单也很方便
  form.username = ''
  form.password = ''
}
</script>

场景2:单独的计数器和开关

这时候用ref更合适:

<template>
  <div>
    <p>计数器:{{ count }}</p>
    <button @click="count++">加1</button>
    <div v-if="isShow">我是显示的内容</div>
    <button @click="isShow = !isShow">切换显示</button>
  </div>
</template>
<script setup>
import { ref } from 'vue'
// 单独的变量用ref
const count = ref(0)
const isShow = ref(true)
</script>

新手用reactive最容易踩的3个大坑,一定要避开

虽然reactive很好用,但新手如果不注意细节,很容易踩坑,今天给大家讲3个最常见的,只要避开这3个,reactive基本上就不会出问题了。

坑1:直接给reactive对象赋值整个对象,会丢失响应式

这个是新手踩得最多的坑,没有之一,很多人会觉得“既然user是响应式的,那我直接给user赋一个新的普通对象,它应该还是响应式的吧?”错!因为reactive返回的是一个代理对象,如果你直接给它赋值一个新的普通对象,相当于把原来的代理对象替换掉了,新的普通对象没有经过reactive处理,自然就没有响应式了。

举个错误的例子:

<template>
  <div>
    <p>姓名:{{ user.name }}</p>
    <button @click="updateUserWrong">错误的更新方式</button>
    <button @click="updateUserRight">正确的更新方式</button>
  </div>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
  name: '赵六',
  age: 30
})
// 错误!直接赋值整个普通对象,丢失响应式
const updateUserWrong = () => {
  user = {
    name: '孙七',
    age: 31
  }
  console.log(user) // 这时候user已经变成普通对象了,不是代理对象
}
// 正确!逐个修改属性,或者用Object.assign,或者用扩展运算符(但要注意是修改原对象的属性,不是替换对象)
const updateUserRight = () => {
  // 方式1:逐个修改
  // user.name = '孙七'
  // user.age = 31
  // 方式2:用Object.assign,会把新对象的属性合并到原代理对象上
  Object.assign(user, {
    name: '孙七',
    age: 31
  })
  // 方式3:注意!不能直接写user = {...user, name: '孙七', age: 31},这还是替换对象
  // 但如果你的对象是嵌套的,内层属性修改可以用扩展,但外层还是要合并
}
</script>

你可以试试点击第一个按钮,页面上的姓名不会变;点击第二个按钮,页面就会正常更新了,这里要特别提一下Object.assign和扩展运算符的区别:Object.assign是修改第一个参数对象,把后面的对象的属性合并进去,所以如果第一个参数是reactive的代理对象,那合并之后还是响应式的;而扩展运算符如果直接用来赋值整个对象,比如user = {...user, name: '孙七'},是创建一个新的普通对象,然后替换原来的代理对象,所以会丢失响应式,这点一定要记住!

坑2:解构reactive对象会丢失响应式

很多人喜欢用解构赋值来简化代码,比如从user对象里解构出name和age,但是如果你直接解构reactive的代理对象,解构出来的变量会变成普通的基本类型或者引用类型,不再有响应式了。

举个错误的例子:

<template>
  <div>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <button @click="updateUser">更新信息</button>
  </div>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({
  name: '周八',
  age: 32
})
// 错误!直接解构,name和age变成普通变量,没有响应式
const { name, age } = user
const updateUser = () => {
  user.age++
  console.log(age) // age还是32,没有跟着变
}
</script>

你可以试试点击按钮,页面上的age不会变,因为解构出来的age是普通的数字,和user.age已经没有关系了,那如果我就是想用解构怎么办?这时候可以用Vue3提供的toRefs或者toRef函数。

toRefs的用法

toRefs会把reactive对象的所有属性都转换成ref对象,这些ref对象会和原reactive对象的属性保持双向绑定——修改ref对象的.value,原reactive对象的属性会变;修改原reactive对象的属性,ref对象的.value也会变,这样解构出来的ref对象就有响应式了。

举个正确的例子:

<template>
  <div>
    <p>姓名:{{ name }}</p>
    <p>年龄:{{ age }}</p>
    <button @click="updateUser1">通过解构的ref更新</button>
    <button @click="updateUser2">通过原对象更新</button>
  </div>
</template>
<script setup>
import { reactive, toRefs } from 'vue'
const user = reactive({
  name: '吴九',
  age: 33
})
// 正确!用toRefs转换成ref对象再解构
const { name, age } = toRefs(user)
const updateUser1 = () => {
  // 解构出来的是ref对象,要加.value
  age.value++
}
const updateUser2 = () => {
  user.age++
  console.log(age.value) // 会跟着变成34
}
</script>

toRef的用法

如果你只需要解构reactive对象的某一个属性,不需要所有属性,这时候用toRef更合适,它只会把指定的属性转换成ref对象,同样保持双向绑定。

import { reactive, toRef } from 'vue'
const user = reactive({
  name: '郑十',
  age: 34
})
// 只解构name
const name = toRef(user, 'name')
const updateName = () => {
  name.value = '郑十一'
  console.log(user.name) // 会变成郑十一
}

toRef还有一个好处:如果原reactive对象里没有你指定的属性,toRef会给你创建一个值为undefined的ref对象,而且这个ref对象也会和原对象保持双向绑定——之后你给这个ref对象的.value赋值,原对象会自动新增这个属性,并且有响应式;反过来,如果原对象之后新增了这个属性,ref对象的.value也会跟着变,这点和直接从原对象解构不一样,直接解构不存在的属性会得到undefined,而且之后原对象新增了也不会变。

坑3:reactive对象的嵌套属性如果是基本类型,被单独取出来也会丢失响应式?

其实这个坑和坑2有点类似,但不是完全一样,比如你有一个嵌套的reactive对象:

const user = reactive({
  name: '钱十二',
  profile: {
    age: 35,
    address: '广州天河区'
  }
})

如果你单独把user.profile取出来赋值给一个变量,比如const profile = user.profile,那这个profile还是响应式的,因为它本身也是一个reactive生成的代理对象(Vue3的reactive是深层响应式的,会递归处理对象的所有嵌套属性),但是如果你把user.profile.age取出来赋值给一个变量,比如const age = user.profile.age,那这个age就是普通的数字,没有响应式了,因为它是基本类型,和坑2的情况一样。

所以如果要单独使用嵌套属性的基本类型,也可以用toRef:

const age = toRef(user.profile, 'age')

这样age就是一个响应式的ref对象了。

总结一下reactive的关键知识点

最后给大家整理一下今天讲的核心内容,方便大家记忆:

  1. reactive的底层原理:用ES6的Proxy对象实现深层响应式,能监听对象/数组的所有操作(读取/修改已有属性、新增/删除属性、索引修改、length修改、所有方法调用),解决了Vue2的两个硬伤。
  2. reactive的适用范围:只能处理引用类型(对象、数组、Map、Set),不能处理基本类型。
  3. reactive和ref的选择:逻辑相关的一组属性用reactive,单独的变量用ref;如果需要解构reactive对象,用toRefs/toRef处理。
  4. 新手必避的3个大坑
    • 不要直接给reactive对象赋值整个普通对象,要用Object.assign合并属性或者逐个修改。
    • 不要直接解构reactive对象,要用toRefs(全部属性)或者toRef(单个属性)转换成ref对象再解构。
    • 单独取出嵌套属性的基本类型时,要用toRef处理,否则会丢失响应式。

好了,今天关于Vue3 reactive的内容就讲到这里了,如果你还有其他问题,欢迎在评论区留言交流,我会尽量回复大家的,如果觉得这篇文章对你有帮助,别忘了点赞、收藏、转发给身边正在学Vue3的朋友哦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门