深入了解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会记录下数据的变化,并准备更新视图。

watchervue2响应式系统的"中枢神经系统",扮演着重要的角色,它主要会承担三大核心职责,

1.收集依赖 2.变更检测 3.更新调度,这里我就不细讲了大家可以去了解一下

但是需要注意的是,vue不能检测到数组和对象的变化,这是因为我们是通过索引来进行更新赋值的,但是索引赋值我们的更新不会触发setter函数,因为只有当我们的数据更新触发了setter函数,才能实现响应式更新,但是当我们使用数组的一些方法的时候,比如**push()、pop()、shift()、unshift()、splice()、sort()、reverse()**等。这些方法会触发响应式更新。

下面举一个例子:

html 复制代码
<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 进行状态管理,它会使用gettersetter 方法来进行管理,因为简单类型的响应式操作比较简单,proxy 中会封存很多的方法,所以我们不在proxy中实现响应式数据更新,这样可以提高性能,而且proxy也只能用在数组或者对象这种类型,不能用于简单类型的。

javascript 复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

track是一个核心函数,用于在访问响应式对象属性的时候进行收集依赖

使用effect()定义一个副作用函数,该函数会读取 数据的值并打印到控制台。当 effect() 执行时,会将当前函数设置为activeEffect,并在读取数据时触发track函数,完成依赖收集。

trigger函数是用于触发依赖更新的关键部分。它的主要作用是:当数据发生变化时,通知所有依赖该数据的副作用函数(如组件的渲染函数)重新执行,从而实现视图的自动更新。

javascript 复制代码
// 依赖收集和触发相关的全局变量
const targetMap = new WeakMap();

// 当前活跃的副作用函数
let activeEffect = null;

// 用于收集依赖的函数
function track(target, key) {
    if (activeEffect) {
        // 获取与目标对象关联的依赖映射,如果没有则创建一个新的 Map
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        // 获取与属性名关联的依赖集合,如果没有则创建一个新的 Set
        let 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 可直接响应 MapSetWeakMap 等集合类型的变化
调试能力 较弱 增强的调试钩子(onTrack/onTrigger Vue 3 提供响应式依赖追踪和触发调试工具
相关推荐
koooo~11 分钟前
【无标题】
前端
Attacking-Coder43 分钟前
前端面试宝典---前端水印
前端
姑苏洛言3 小时前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
烛阴3 小时前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
Alice_hhu4 小时前
ResizeObserver 解决 echarts渲染不出来,内容宽度为 0的问题
前端·javascript·echarts
charlee445 小时前
解决Vditor加载Markdown网页很慢的问题(Vite+JS+Vditor)
javascript·markdown·cdn·vditor
逃逸线LOF5 小时前
CSS之动画(奔跑的熊、两面反转盒子、3D导航栏、旋转木马)
前端·css
老马啸西风5 小时前
工作流引擎-18-开源审批流项目之 plumdo-work 工作流,表单,报表结合的多模块系统
vue.js·开源·activiti·workflow·flowable·oa·bpm
萌萌哒草头将军5 小时前
⚡️Vitest 3.2 发布,测试更高效;🚀Nuxt v4 测试版本发布,焕然一新;🚗Vite7 beta 版发布了
前端
技术小丁6 小时前
使用 HTML + JavaScript 在高德地图上实现物流轨迹跟踪系统
前端·javascript·html