Vue2 和 Vue3 响应式原理与差别 写 Vue 的时候我们最熟悉的一件事就是数据变了页面会自动更新。比如在 Vue2 里data(){return{count:0}}模板里使用div{{ count }}/div当我们执行this.count页面上的数字就会跟着变。这件事看起来很自然但它背后其实是 Vue 的响应式系统在工作。Vue2 和 Vue3 都能实现“数据驱动视图”但底层实现方式差别很大。这篇文章就来聊聊 Vue2 和 Vue3 的响应式原理、实际开发中的区别。一、响应式到底是什么响应式不是简单地说“数据变了页面就变了”。更准确一点说响应式解决的是当某个数据被使用时Vue 记录谁依赖了它当这个数据变化时Vue 再通知这些依赖重新执行。可以拆成两步读取数据时收集依赖。修改数据时触发更新。比如模板中用了count组件渲染时会读取countVue 就知道这个组件依赖了count。之后count改变Vue 就通知这个组件重新渲染。所以响应式的核心不是“数据会变”而是Vue 如何知道谁用到了这个数据以及数据变化后该通知谁。二、Vue2 的响应式原理Vue2 的响应式核心是Object.defineProperty。在组件初始化时Vue2 会遍历data中的属性把每个属性都转换成带有getter和setter的形式。简化代码大概是这样functiondefineReactive(obj,key,value){Object.defineProperty(obj,key,{get(){// 读取属性时收集依赖returnvalue},set(newValue){if(newValue!value){valuenewValue// 修改属性时通知更新}}})}当页面渲染时读取this.count会触发getVue 在这里收集依赖。当我们修改this.count1会触发setVue 在这里通知页面更新。Vue2 内部大致有几个重要角色Observer把普通对象转换成响应式对象。Dep依赖收集器用来保存依赖。Watcher订阅者比如组件渲染 watcher、计算属性 watcher、用户写的 watch。可以简单理解成data 属性 - Dep - Watcher - 组件更新三、Vue2 响应式的限制Vue2 的响应式能满足大多数场景但因为它基于Object.defineProperty所以有一些比较典型的限制。1. 对象新增属性不是响应式的比如data(){return{user:{name:Tom}}}后面再写this.user.age18在 Vue2 中age不是初始化时就存在的属性所以 Vue 没有给它设置getter和setter。因此这个新增属性不会自动变成响应式。正确写法通常是this.$set(this.user,age,18)或者Vue.set(this.user,age,18)2. 对象删除属性也不会自动响应比如deletethis.user.name这种写法在 Vue2 中不会稳定触发视图更新。需要使用this.$delete(this.user,name)3. 数组索引修改有问题Vue2 对数组做了特殊处理。它重写了数组的几个变更方法比如push pop shift unshift splice sort reverse所以这些写法是可以触发更新的this.list.push(1)this.list.splice(0,1,new value)但是下面这种写法在 Vue2 中不会稳定触发视图更新this.list[0]new value应该改成this.$set(this.list,0,new value)或者this.list.splice(0,1,new value)4. 修改数组 length 也有问题比如this.list.length0在 Vue2 中这种写法也不会稳定触发响应式更新。更推荐写成this.list.splice(0)这些就是 Vue2 里经常遇到的“数据明明变了页面怎么没更新”的来源。四、Vue3 的响应式原理Vue3 的响应式核心换成了Proxy。Proxy可以代理整个对象而不是只能劫持对象中已经存在的某个属性。简化代码可以这样理解conststatenewProxy({count:0},{get(target,key,receiver){// 读取属性时收集依赖returnReflect.get(target,key,receiver)},set(target,key,value,receiver){// 修改属性时触发更新returnReflect.set(target,key,value,receiver)}})当读取state.count会触发get。当修改state.count1会触发set。Vue3 内部可以简单理解成reactive 数据 - Proxy 拦截读取和修改 - track 收集依赖 - trigger 触发更新其中track读取数据时收集依赖。trigger修改数据时触发依赖。effect副作用函数数据变化后会重新执行。比如import{reactive,effect}fromvueconststatereactive({count:0})effect((){console.log(state.count)})state.count第一次执行effect时会读取state.count于是 Vue 收集到依赖。后面执行state.count时会触发更新effect重新执行。在真实组件里我们一般不会直接写effect但组件渲染、computed、watch底层都和这套机制有关。五、Vue3 中 ref 和 reactive 的区别Vue3 里最常见的两个响应式 API 是ref和reactive。reactivereactive通常用来处理对象conststatereactive({count:0,user:{name:Tom}})使用时直接访问属性state.countstate.user.nameJerryreactive的底层主要依赖Proxy。refref通常用来处理基本类型也可以处理对象constcountref(0)count.value在 JavaScript 中使用ref需要通过.value访问console.log(count.value)但在模板中Vue 会自动解包div{{ count }}/div这里不需要写.value。需要注意的是ref本身不是简单地等于Proxy。它更像是把值包装成一个带有.value的响应式对象通过访问.value来进行依赖收集和更新触发。六、Vue2 和 Vue3 响应式的核心区别1. 底层实现不同Vue2 使用Object.definePropertyVue3 使用Proxy这是最根本的区别。Vue2 是劫持对象中已经存在的属性。Vue3 是代理整个对象所以能拦截更多操作。比如 Vue3 可以监听state.user.age18deletestate.user.name state.list[0]new valuestate.list.length0这些在 Vue2 中都是比较容易踩坑的地方。2. 对新增属性的支持不同Vue2 中新增属性需要使用this.$set(obj,key,value)Vue3 中可以直接写obj[key]value因为Proxy可以拦截对象属性的新增操作。3. 对删除属性的支持不同Vue2 中删除属性要使用this.$delete(obj,key)Vue3 中可以直接写deleteobj[key]4. 对数组的支持不同Vue2 中数组通过重写部分方法实现响应式。所以这些方法可以触发更新this.list.push(item)this.list.splice(index,1,newValue)但索引赋值和修改length有问题this.list[0]new valuethis.list.length0Vue3 使用Proxy后对数组索引和length的监听更自然state.list[0]new valuestate.list.length0这些都可以触发响应式更新。5. 对 Map、Set 的支持不同Vue2 对Map、Set这类数据结构支持不好。Vue3 对集合类型做了专门处理所以可以这样写constmapreactive(newMap())map.set(name,Tom)console.log(map.get(name))Map、Set的读取、添加、删除等操作都可以参与响应式。6. 初始化方式不同Vue2 初始化时会递归遍历data把里面的属性都转换成响应式。如果数据层级很深、对象很大初始化成本会比较高。Vue3 使用Proxy后响应式处理更偏向按需进行。嵌套对象通常是在访问到的时候才继续转换成响应式代理。所以 Vue3 在响应式初始化上更加灵活。7. 响应式能力是否独立Vue2 的响应式更多是围绕组件实例设计的this.countthis.userthis.$setVue3 的响应式能力可以脱离组件单独使用constcountref(0)conststatereactive({name:Tom})这也是 Composition API 能成立的基础。我们可以把一段逻辑封装成 hookimport{ref}fromvueexportfunctionuseCounter(){constcountref(0)constadd(){count.value}return{count,add}}这种逻辑复用方式在 Vue3 里会比 Vue2 自然很多。七、开发中常见的坑1. Vue2 中字段要提前声明Vue2 项目里最好提前把页面会用到的字段写完整data(){return{form:{username:,password:,age:null}}}不要接口回来后才随手新增this.form.age18如果确实要新增使用this.$set(this.form,age,18)2. Vue2 修改数组时优先使用 splice不要直接写this.list[index]newValue推荐写this.list.splice(index,1,newValue)或者this.$set(this.list,index,newValue)3. Vue3 中 reactive 解构会丢响应式这是 Vue3 中非常常见的坑。比如conststatereactive({count:0})const{count}state这样解构出来的count是一个普通值不再是响应式的。如果需要解构并保持响应式可以使用import{toRefs}fromvueconststatereactive({count:0})const{count}toRefs(state)之后使用count.value或者干脆不解构直接使用state.count4. ref 在 JavaScript 中要写 .valueVue3 中constcountref(0)在 JavaScript 中修改要写count.value模板里可以直接写div{{ count }}/div这是因为模板中 Vue 做了自动解包。刚从 Vue2 切到 Vue3 的时候忘记.value很常见。5. 不要把 reactive 整体替换掉比如letstatereactive({count:0})state{count:1}这样会让原来的响应式引用丢失。更推荐修改属性state.count1如果确实需要整体替换一份对象很多时候可以考虑用refconststateref({count:0})state.value{count:1}八、面试怎么回答如果面试官问说一下 Vue2 和 Vue3 响应式原理的区别。可以这样回答Vue2 的响应式基于Object.defineProperty。组件初始化时Vue 会递归遍历data中的属性通过getter做依赖收集通过setter触发更新。它的问题是只能劫持已经存在的属性所以对象新增和删除属性无法自动响应。数组方面Vue2 是通过重写push、pop、splice等方法来实现响应式的但直接通过索引修改数组或者修改length不会稳定触发更新。Vue3 的响应式主要基于Proxy。它代理的是整个对象可以拦截属性读取、设置、删除、遍历等操作。读取数据时通过track收集依赖修改数据时通过trigger触发更新。相比 Vue2Vue3 对对象新增删除、数组索引、数组长度、Map、Set等支持更好而且响应式系统更加独立可以配合 Composition API 做逻辑复用。如果想回答得更完整一点可以补充Vue3 中reactive主要基于Proxy而ref是通过.value的访问来进行依赖收集和触发更新。Vue3 的响应式系统相比 Vue2 更灵活也更适合抽离公共逻辑。九、简单总结Vue2 和 Vue3 响应式的目标是一样的都是让数据变化自动驱动视图更新。它们最大的区别在底层实现Vue2Object.defineProperty Vue3ProxyVue2 的响应式模型比较经典也比较容易理解但因为Object.defineProperty的限制在对象新增属性、删除属性、数组索引修改、数组 length 修改等场景下容易踩坑。Vue3 使用Proxy后能监听的操作更多对对象、数组、Map、Set的支持也更完整。同时Vue3 把响应式能力做得更独立配合ref、reactive、computed、watch可以更方便地组织和复用逻辑。日常开发里如果写 Vue2要记住新增属性用 $set 删除属性用 $delete 数组修改尽量用 splice data 字段尽量提前声明如果写 Vue3要记住ref 在 JS 里要用 .value reactive 不要随便解构 需要解构时可以用 toRefs 整体替换对象时可以考虑 ref理解这些以后再遇到“数据变了但页面没更新”的问题就不会只靠猜了。