深入了解Vue2和Vue3的响应式原理
想必大家在学习vue的时候都会有这样的疑问,自己在学习JavaScript的时候,不论要修改什么内容,只有在页面刷新的时候,我们的值才会发生更新变化,但是当我们在一个vue项目中进行一样的操作的时候,就可以实现实时的变化,这是为什么呢,这是因为vue当中可以实现响应式数据更新,什么是响应式数据更新?到底是通过什么来进行实现的呢?
vue2:
vue2中是通过Object.defineProperty () 来实现响应式数据更新的,当你创建了一个vue对象,并且向它的data里面添加了部分数据,这些就变成了响应式数据,vue会遍历所有的数据,然后使用Object.defineProperty () 把这些数据添加上getter和setter,这样再我们的vue当中就会自动追踪依赖,当这些数据property被访问和修改时进行通知变更。所以就是说,我们的响应式是通过getter和setter来实现数据实时更新的,当响应式数据的值通过setter被修改时,vue 会通知所有与此数据相关的所有 Watcher。这些 Watcher 会记录下数据的变化,并准备更新视图。
watcher是vue2响应式系统的"中枢神经系统",扮演着重要的角色,它主要会承担三大核心职责,
1.收集依赖 2.变更检测 3.更新调度,这里我就不细讲了大家可以去了解一下
但是需要注意的是,vue不能检测到数组和对象的变化,这是因为我们是通过索引来进行更新赋值的,但是索引赋值我们的更新不会触发setter函数,因为只有当我们的数据更新触发了setter函数,才能实现响应式更新,但是当我们使用数组的一些方法的时候,比如 push()、pop()、shift()、unshift()、splice()、sort()、reverse()
等。这些方法会触发响应式更新。
下面举一个例子:
<div id="app"><ul><!-- 使用 v-for 指令渲染数组 --><li v-for="(item, index) in list" :key="index">{{ item }}</li></ul><button @click="updateByIndex">直接索引修改</button><button @click="updateBySplice">用 splice 修改</button>
</div><script>new Vue({el: '#app',data: {list: [1, 2, 3]},methods: {// 直接使用索引赋值修改数组元素updateByIndex() {this.list[0] = 4;console.log('索引赋值后数组:', this.list);},// 使用 splice 方法修改数组元素updateBySplice() {this.list.splice(0, 1, 4);console.log('splice 修改后数组:', this.list);}}});
</script>
这里实现的效果就是,当我们调用updateByIndex()方法的时候,我们的数组的值会被修改,但是我们的vue页面不会监听到这个数据的变化,就不会更新,打印的时候是【4,2,3】,但是页面上显示的还是【1,2,3】,但是当我们去调用updateBySplice()这个方法,不管是我们的打印还是页面上的更新,都显示的【4,2,3】,这就说明了我们的splice可以调用我们的setter方法从而实现页面的响应式数据更新,这个情况就可以证明的想法
vue3:
vue3中是通过proxy来实现的响应式对象,我们在vue3中实现响应式对象的方法有两个,一个是ref,一个是reactive,这两个的区别就是ref可以用于所有的类型,但是reactive只能用于数组和对象类型,ref如果封装的是一个简单类型的对象,那么它不会使用proxy进行状态管理,它会使用getter和setter方法来进行管理,因为简单类型的响应式操作比较简单,proxy中会封存很多的方法,所以我们不在proxy中实现响应式数据更新,这样可以提高性能,而且proxy也只能用在数组或者对象这种类型,不能用于简单类型的。
function reactive(obj) {return new Proxy(obj, {get(target, key) {track(target, key)return target[key]},set(target, key, value) {target[key] = valuetrigger(target, key)}})
}function ref(value) {const refObject = {get value() {track(refObject, 'value')return value},set value(newValue) {value = newValuetrigger(refObject, 'value')}}return refObject
}
track是一个核心函数,用于在访问响应式对象属性的时候进行收集依赖
使用effect()定义一个副作用函数,该函数会读取 数据的值并打印到控制台。当 effect()
执行时,会将当前函数设置为activeEffect,并在读取数据时触发track函数,完成依赖收集。
trigger函数是用于触发依赖更新的关键部分。它的主要作用是:当数据发生变化时,通知所有依赖该数据的副作用函数(如组件的渲染函数)重新执行,从而实现视图的自动更新。
// 依赖收集和触发相关的全局变量
const targetMap = new WeakMap();// 当前活跃的副作用函数
let activeEffect = null;// 用于收集依赖的函数
function track(target, key) {if (activeEffect) {// 获取与目标对象关联的依赖映射,如果没有则创建一个新的 Maplet depsMap = targetMap.get(target);if (!depsMap) {targetMap.set(target, (depsMap = new Map()));}// 获取与属性名关联的依赖集合,如果没有则创建一个新的 Setlet dep = depsMap.get(key);if (!dep) {depsMap.set(key, (dep = new Set()));}// 将当前活跃的副作用函数添加到依赖集合中dep.add(activeEffect);}
}// 用于触发依赖更新的函数
function trigger(target, key) {// 获取与目标对象关联的依赖映射const depsMap = targetMap.get(target);if (depsMap) {// 获取与被修改属性关联的依赖集合const dep = depsMap.get(key);if (dep) {// 遍历依赖集合,执行每个副作用函数dep.forEach(effect => {effect();});}}
}// 创建响应式对象的函数
function reactive(target) {return new Proxy(target, {get(target, key, receiver) {// 触发依赖收集track(target, key);const res = Reflect.get(target, key, receiver);// 如果属性值是对象,则递归创建响应式对象if (typeof res === 'object' && res !== null) {return reactive(res);}return res;},set(target, key, value, receiver) {const oldValue = target[key];// 使用 Reflect.set 修改属性值const result = Reflect.set(target, key, value, receiver);// 如果新旧值不同,则触发依赖更新if (oldValue !== value) {trigger(target, key);}return result;}});
}// 定义副作用函数的函数
function effect(fn) {// 将传入的函数作为当前活跃的副作用函数activeEffect = fn;// 执行副作用函数,进行依赖收集fn();// 执行完成后,重置当前活跃的副作用函数activeEffect = null;
}// 示例:创建一个响应式对象并定义一个副作用函数
const state = reactive({ count: 0 });// 定义一个副作用函数,每当 state.count 变化时,会重新执行
effect(() => {console.log(`count is: ${state.count}`);
});// 修改 state.count,触发 trigger 函数,重新执行副作用函数
state.count = 1; // 输出:count is: 1
state.count = 2; // 输出:count is: 2
区别:
特性 | Vue 2 | Vue 3 | 核心差异说明 |
---|---|---|---|
实现原理 | Object.defineProperty | Proxy | Proxy 直接代理整个对象,无需递归初始化属性 |
数组响应式 | 需重写数组方法(push/pop/shift 等) | 原生支持数组索引/长度修改 | Vue 3 可直接通过索引修改数组(如 arr[0]=1 )或修改 length |
动态新增属性 | 用Vue.set() / Vue.delete() | 直接赋值生效 | Vue 3 中 obj.newProperty = value 自动触发响应式 |
嵌套对象初始化 | 初始化时递归遍历所有属性 | 按需惰性代理(访问时触发) | Vue 3 减少初始化开销,提升性能 |
性能优化 | 初始化时递归所有数据,性能压力较大 | 惰性代理 + 缓存机制,内存占用更低 | 大型对象/数组场景下 Vue 3 性能优势明显 |
响应式 API | 仅通过 data 选项 | 提供 ref() / reactive() 等独立 API | Vue 3 可在组件外灵活创建响应式数据 |
Map/Set 集合支持 | 不支持 | 原生支持 | Vue 3 可直接响应 Map 、Set 、WeakMap 等集合类型的变化 |
调试能力 | 较弱 | 增强的调试钩子(onTrack /onTrigger ) | Vue 3 提供响应式依赖追踪和触发调试工具 |