Vue3响应式到底和Vue2比有啥不一样?新手能快速上手吗?怎么避开常见坑?
很多刚转Vue3或者还在观望的前端小伙伴,估计刷教程刷到一半就被“Proxy”“Reflect”这些词绕晕,心里满是问号:老版本的Object.defineProperty明明用着也还行,怎么非要换个技术栈升级响应式的底层逻辑?是不是新手更难搞了?写代码的时候遇到ref和reactive傻傻分不清楚,会不会经常踩雷?别慌,今天咱们就用大白话唠明白这些问题,从基础认知到避坑指南,全都是日常开发摸爬滚打出来的干货。
为啥Vue3要换掉Vue2的响应式核心?
先别管Proxy和Object.defineProperty的具体代码差别,说个大家开发中能直接摸到的好处——你有没有在Vue2里遇到过「明明改了数组长度或者深层对象属性,但页面就是不刷新的情况?
// Vue2里常踩的坑 this.list[0] = '新内容'; this.list.length = 0; this.userInfo.address.street = '更新后的街道';
第一个数组改索引,第二个数组重置长度,Vue2都得用特定的$set、$delete或者数组变异方法才能触发响应;第三个深层对象嵌套太深,要是初始化时没定义address或者street的getter/setter,Vue2也没办法直接监听新增的属性,而Vue3用了Proxy之后,这些场景直接“一竿子解决”——不管你是改索引、重置长度、新增/删除深层对象属性,都能自动捕获到变化更新页面,根本不用记那些额外的API。
底层原理上,Object.defineProperty和Proxy到底差在哪?
简单说,Object.defineProperty就像给对象的“每个口袋拉链”——你得提前把每个要监听的口袋(属性)都装个拉链(getter/setter),要是哪天偷偷塞个新口袋或者剪个新口袋拉链,拉链是不会自动装上的,得手动操作,而Proxy呢?它是给整个对象(或者数组)套了个“智能监控套”,不管你摸口袋、塞口袋、剪口袋,甚至把口袋套套再套口袋,监控套都能第一时间知道你做了啥,然后通知页面更新。
哦对了,Object.defineProperty只能给单个属性监听,要是你的对象有1000个属性,就得循环1000次给每个装拉链,性能自然会下降;Proxy直接监听整个对象,初始化速度会快很多,尤其是大对象多的项目,这个优势就更明显了。
新手入门Vue3响应式,先搞懂ref和reactive这对“黄金搭档”就行?
没错,新手不用一开始就死磕Proxy和Reflect的源码细节,先把日常开发用得最多的ref和reactive搞明白、用顺手,响应式这关就算过了80%。
ref是什么?什么时候用ref?
你可以把ref理解成“一个带响应式开关的盒子”,不管盒子里装的是基本类型(数字、字符串、布尔值、undefined、null、Symbol),还是引用类型(对象、数组、函数),它都能把这个东西包起来,让它变成响应式的。
那什么时候用ref?
- 当你要定义基本类型的响应式数据时,必须用ref——因为Proxy只能套引用类型,基本类型套不上盒子,只能用ref先装个壳。
- 当你要定义引用类型但之后可能会完全替换掉它时,建议用ref——比如你从接口拿回来的数据先赋值给一个空数组,之后又重新赋值成一个新的具体数组,用reactive的话直接替换会丢失响应式,但ref替换整个盒子里的东西就行,因为监控套会自动套在盒子本身?不对不对,是ref会在盒子内部把引用类型自动转换成proxy,然后监控盒子的value属性,所以整个替换value是没问题的。
- 当你要把一个响应式数据传给子组件作为props的一部分,或者要用v-model绑定单个值时,建议用ref——v-model其实就是v-model:value的简写,ref直接.value就对应了value属性,更顺手。
给大家举个ref的小例子:
import { ref } from 'vue';
setup() {
// 基本类型,必须用ref
const count = ref(0);
// 引用类型,也可以用ref
const user = ref({ name: '张三', age: 18 });
// 注意哦,在setup或者script setup语法糖里(推荐用script setup),修改ref数据要加.value
const addCount = () => {
count.value++;
};
const updateUserAge = () => {
// 引用类型的ref,内部属性不用加.value,直接改就行
user.value.age++;
};
const replaceUser = () => {
// 完全替换整个引用类型的ref也没问题
user.value = { name: '李四', age: 25 };
};
return { count, user, addCount, updateUserAge, replaceUser };
}
哦对了,如果你用的是Vue3的<script setup>语法糖(现在基本都是用这个了),修改ref数据要加.value,但在模板里不用,Vue3会自动帮你解包,直接写{{ count }}或者@click="addCount"就行,非常方便。
reactive是什么?什么时候用reactive?
你可以把reactive理解成“一个直接套了智能监控套的引用类型”,它只能套对象、数组、Map、Set这些引用类型,不能套基本类型,套基本类型会报错。
那什么时候用reactive?
- 当你要定义一组相关的引用类型响应式数据时,建议用reactive——比如用户的所有信息(姓名、年龄、手机号、地址)放在一个user对象里,表单的所有字段放在一个form对象里,这样写代码会更清晰,不用一个个去定义ref。
- 当你不需要完全替换掉整个引用类型数据时,建议用reactive——因为完全替换的话会丢失响应式,比如你定义了一个reactive的form对象,之后直接把form = { name: '' }赋值,这个form就不再是响应式的了,这点一定要注意。
- 当你要解构一个响应式对象但又不想失去响应式时,可以用reactive配合toRefs——比如你从接口拿回来的数据放在一个reactive的result对象里,里面有data、code、msg三个属性,你可以用toRefs把这三个属性都变成ref,这样解构之后每个属性还是响应式的。
给大家举个reactive的小例子:
import { reactive, toRefs } from 'vue';
setup() {
// 一组相关的引用类型数据,用reactive
const userForm = reactive({
name: '',
age: 18,
gender: '男',
address: reactive({
// 嵌套对象也可以用reactive再套一层,不过其实不用,外层的已经能监控到深层了
province: '',
city: '',
street: ''
})
});
// 完全替换整个userForm会丢失响应式!!!
// userForm = { name: '王五' }; 这种写法是错的
// 正确的更新方式
const updateForm = () => {
userForm.name = '王五';
userForm.address.street = '更新后的街道';
};
// 解构配合toRefs
const { name, age } = toRefs(userForm);
// 这样修改name.value,userForm.name也会跟着变,保持响应式
const changeName = () => {
name.value = '赵六';
};
return { userForm, updateForm, name, age, changeName };
}
Vue3响应式新手必踩的5个坑,提前避坑少走弯路!
刚学Vue3响应式的小伙伴,很容易踩这几个坑,今天给大家列出来,提前记牢:
第一个坑:给reactive对象直接赋值,丢失响应式
刚才在讲reactive的时候已经提到过了,比如你定义了一个reactive的list = reactive([1,2,3]),之后直接list = [4,5,6],这个list就不再是响应式的了,正确的做法有两个:
- 用splice替换整个数组的内容:list.splice(0, list.length, 4,5,6);
- 直接改list的每个索引:list[0] = 4; list[1] =5; list[2] =6;
- 最推荐的做法是用ref替换reactive,这样直接替换list.value = [4,5,6]就行,完全没问题。
第二个坑:在setup或者script setup里忘记给ref数据加.value
这个是新手最容易犯的错误,尤其是刚从Vue2转过来的小伙伴,Vue2里修改data里的数据直接this.xxx就行,不用加任何东西,但在Vue3的setup或者script setup里,ref数据本质上是一个对象,里面有一个value属性才是真正的内容,所以修改的时候必须加.value,模板里不用,Vue3会自动解包。
第三个坑:解构reactive对象没有用toRefs,失去响应式
比如你定义了一个reactive的user = reactive({ name: '张三', age: 18 }),之后直接const { name, age } = user,这样name和age只是普通的字符串和数字,修改它们不会更新页面,正确的做法是用toRefs把它们都变成ref:const { name, age } = toRefs(user),这样修改name.value,user.name也会跟着变,保持响应式。
第四个坑:ref包裹引用类型时,内部属性不用加.value
刚才在讲ref的时候也提到过,比如你定义了一个ref的user = ref({ name: '张三', age: 18 }),之后修改user.name是错的,要修改user.value.name;但修改user.value.name不用再加.value,直接改就行,因为内部的对象已经被ref自动转换成了proxy。
第五个坑:用watch监听ref的基本类型和引用类型的写法不一样
比如你定义了一个ref的count = ref(0),监听它的变化直接watch(count, (newVal, oldVal) => { console.log(newVal, oldVal); })就行;但如果你定义了一个ref的user = ref({ name: '张三', age: 18 }),直接watch(user)只能监听整个user.value被替换的情况,不能监听内部属性的变化,要监听内部属性的变化,需要传第三个参数{ deep: true },或者直接watch(() => user.value.name, ...)监听单个属性,如果是reactive的对象,直接watch(user)默认就是深度监听的,不用加deep。
给大家举个watch的小例子:
import { ref, reactive, watch } from 'vue';
setup() {
const count = ref(0);
const userRef = ref({ name: '张三', age: 18 });
const userReactive = reactive({ name: '李四', age: 20 });
// 监听ref的基本类型
watch(count, (newVal, oldVal) => {
console.log('count变化了:', newVal, oldVal);
});
// 监听ref的引用类型的内部属性变化,必须加deep
watch(userRef, (newVal, oldVal) => {
console.log('userRef内部变化了:', newVal, oldVal);
}, { deep: true });
// 监听ref的引用类型的单个属性变化,不用加deep
watch(() => userRef.value.age, (newVal, oldVal) => {
console.log('userRef的age变化了:', newVal, oldVal);
});
// 监听reactive的对象,默认深度监听
watch(userReactive, (newVal, oldVal) => {
console.log('userReactive内部变化了:', newVal, oldVal);
});
const addCount = () => count.value++;
const updateUserRefAge = () => userRef.value.age++;
const updateUserReactiveAge = () => userReactive.age++;
return { count, userRef, userReactive, addCount, updateUserRefAge, updateUserReactiveAge };
}
Vue3响应式新手怎么快速上手?
新手不用一开始就死磕Proxy和Reflect的源码,先做到这几点就行:
- 知道Vue3响应式和Vue2的区别,核心是用了Proxy,解决了Vue2的数组改索引、重置长度、深层对象新增/删除属性不响应的问题,性能也更好;
- 搞懂ref和reactive的用法和适用场景,简单来说就是“基本类型用ref,引用类型尽量用reactive(不需要完全替换的情况下),需要完全替换的引用类型也用ref”;
- 记住新手必踩的5个坑,提前避坑少走弯路;
- 多写多练,写几个小项目自然就熟练了。
哦对了,Vue3还有一些其他的响应式API,比如computed、watchEffect、toRef、shallowRef、shallowReactive等等,这些可以等你把ref和reactive搞明白之后再慢慢学,不用着急一口吃成个胖子。
好了,今天关于Vue3响应式的内容就唠到这里,希望能对大家有所帮助,如果还有什么不懂的问题,欢迎在评论区留言讨论哦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



