Skip to content
字数
1830 字
阅读时间
8 分钟

一、Vue 2 响应式原理:Object.defineProperty

Vue 2 的响应式基于 Object.defineProperty 对对象的属性进行拦截,核心是通过“数据劫持”+“依赖收集”实现视图与数据的联动。

1. 核心机制

  • 数据劫持:对数据对象的每个属性(包括嵌套属性)通过 Object.defineProperty 定义 gettersetter,拦截属性的“读取”和“修改”操作。
  • 依赖收集:当属性被读取时(getter 触发),收集当前依赖(即使用该属性的组件/ Watcher);当属性被修改时(setter 触发),通知所有依赖更新。

2. 实现步骤

  • 初始化响应式:通过 Observer 类将数据对象(data)转为响应式。遍历对象的所有属性,对每个属性调用 Object.defineProperty 定义 getter/setter
    javascript
    class Observer {
      constructor(value) {
        this.value = value;
        this.walk(value); // 遍历对象属性
      }
      walk(obj) {
        Object.keys(obj).forEach(key => {
          defineReactive(obj, key, obj[key]);
        });
      }
    }
    
    function defineReactive(obj, key, val) {
      // 递归处理嵌套对象
      new Observer(val); 
      // 依赖收集器(每个属性对应一个 Dep)
      const dep = new Dep(); 
      Object.defineProperty(obj, key, {
        get() {
          // 收集依赖(当前活跃的 Watcher)
          if (Dep.target) {
            dep.depend();
          }
          return val;
        },
        set(newVal) {
          if (newVal === val) return;
          val = newVal;
          // 通知依赖更新
          dep.notify();
        }
      });
    }
  • 依赖收集器(Dep):每个响应式属性对应一个 Dep 实例,用于存储依赖该属性的 Watcher
  • Watcher:组件渲染、watch 选项、computed 等都会创建 Watcher 实例,当依赖的属性变化时,Watcher 会触发更新(如重新渲染组件)。

视图中会⽤到 data 中某 key,这称为依赖。同⼀个 key 可能出现多次,每次都需要收集出来⽤⼀个 Watcher 来维护它们,此过程称为依赖收集。 多个 Watcher 需要⼀个 Dep 来管理,需要更新时由 Dep 统⼀通知。

3. 局限性

  • 无法监听对象新增/删除属性Object.defineProperty 只能拦截已存在的属性,新增属性(如 this.obj.newKey = 1)或删除属性(如 delete this.obj.key)不会触发更新,需通过 Vue.set/this.$setVue.delete 手动处理。
  • 无法监听数组索引和长度变化:数组的 length 属性修改(如 this.arr.length = 0)或通过索引修改元素(如 this.arr[0] = 1)不会触发更新,Vue 2 对数组的 7 个方法(pushpop 等)进行了重写,以支持响应式。
  • 深度监听性能损耗:初始化时需递归遍历所有属性,对于嵌套较深的大对象,初始化性能较差。

二、Vue 3 响应式原理:Proxy

Vue 3 改用 Proxy 实现响应式,解决了 Vue 2 的诸多局限性,同时保留了“依赖收集”的核心思想。

1. 核心机制

  • 代理拦截:通过 Proxy 创建数据对象的代理(Proxy 实例),拦截对象的所有操作(包括属性读取、修改、新增、删除、数组方法调用等),而非针对单个属性。
  • 懒代理:默认只对当前层级对象创建代理,嵌套对象的代理会在首次访问时动态创建(懒加载),优化初始化性能

WeakMap<target, Map<key, Set<effect>>> - 第一层:WeakMap 的键是被代理的原始对象(target),值是一个 Map(用于映射该对象的属性与依赖)。 - 第二层:Map 的键是对象的属性(key),值是一个 Set(用于存储依赖该属性的所有副作用函数)。 - 第三层:Set 中存储的是具体的副作用函数(effect),即当 target.key 变化时需要执行的函数。

2. 实现步骤

  • 创建响应式对象:通过 reactive 函数创建 Proxy 实例,定义 handler 拦截器对象,覆盖多种操作(getsetdeletePropertyhasownKeys 等)。
    javascript
    function reactive(target) {
      return new Proxy(target, {
        get(target, key, receiver) {
          const res = Reflect.get(target, key, receiver);
          // 收集依赖(与 Vue 2 的 Dep 类似,这里用 track 函数)
          track(target, key);
          // 若属性值是对象,递归创建代理(懒代理)
          if (isObject(res)) {
            return reactive(res);
          }
          return res;
        },
        set(target, key, value, receiver) {
          const oldValue = Reflect.get(target, key, receiver);
          if (oldValue === value) return true;
          const result = Reflect.set(target, key, value, receiver);
          // 通知依赖更新(trigger 函数)
          trigger(target, key);
          return result;
        },
        deleteProperty(target, key) {
          const hadKey = hasOwn(target, key);
          const result = Reflect.deleteProperty(target, key);
          if (hadKey) {
            // 删除属性时通知更新
            trigger(target, key);
          }
          return result;
        }
      });
    }
  • 依赖收集与触发
    • track 函数:在 get 拦截时调用,记录“目标对象 + 属性”与当前依赖(effect 函数)的映射关系。
    • trigger 函数:在 set/deleteProperty 等拦截时调用,根据“目标对象 + 属性”找到所有依赖的 effect 函数并执行(触发更新)。
  • effect 函数:替代 Vue 2 的 Watcher,用于包裹需要响应式执行的逻辑(如组件渲染函数),当依赖的属性变化时,effect 会重新执行。

3. 优势

  • 支持所有对象操作:天然支持新增属性(obj.newKey = 1)、删除属性(delete obj.key)、数组索引修改(arr[0] = 1)、length 修改等,无需手动调用 set/delete
  • 懒代理优化性能:仅在访问嵌套对象时才创建代理,避免 Vue 2 中递归遍历的性能损耗,尤其适合大对象。
  • 更全面的拦截能力Proxy 可以拦截 infor...inObject.keys 等操作,响应式覆盖范围更广。
  • 支持 Map/Set 等集合类型:Vue 3 对 MapSetWeakMapWeakSet 等原生集合类型提供了响应式支持,通过拦截其 getsetadd 等方法实现。

三、核心差异总结

特性Vue 2(Object.defineProperty)Vue 3(Proxy)
拦截粒度单个属性整个对象(所有属性及操作)
新增/删除属性不支持(需手动 $set/$delete原生支持
数组操作仅重写 7 个方法,不支持索引/长度修改支持所有数组操作(索引、长度、方法)
嵌套对象处理初始化时递归遍历懒代理(访问时才创建代理)
集合类型支持不支持(需手动处理)支持 Map/Set 等原生集合
性能(大对象)初始化较慢(全量递归)初始化更快(懒加载)

四、总结

Vue 2 的响应式基于 Object.defineProperty,通过拦截属性的 getter/setter 实现,但存在对新增属性、数组操作的局限性;Vue 3 改用 Proxy 代理整个对象,从根本上解决了这些问题,同时通过懒代理优化性能,支持更全面的响应式场景。这一变化是 Vue 3 性能提升和开发体验优化的重要基础。

贡献者

The avatar of contributor named as chenjie chenjie

页面历史

撰写