深入浅出:Vue2 数据劫持原理剖析

发布于:2025-07-03 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

一、什么是数据劫持?

二、核心 API:Object.defineProperty

三、Vue2 中的数据劫持实现

1. 对象属性的劫持

2. 嵌套对象的处理

3. 数组的特殊处理

四、结合依赖收集的完整流程

五、数据劫持的局限性

六、Vue3 的改进方案

总结


一、什么是数据劫持?

数据劫持指的是拦截对象属性的访问和修改操作的能力。Vue2 通过 JavaScript 的 Object.defineProperty API 实现这一机制,在属性被读取或修改时执行自定义逻辑。

二、核心 API:Object.defineProperty

Object.defineProperty 允许我们精确控制对象属性的行为:

let obj = { name: 'Vue' };
let value = obj.name;

Object.defineProperty(obj, 'name', {
  get() {
    console.log('读取 name 属性');
    return value;
  },
  set(newVal) {
    console.log('设置 name 属性', newVal);
    value = newVal;
  }
});

obj.name; // 控制台输出: "读取 name 属性"
obj.name = 'React'; // 控制台输出: "设置 name 属性 React"

 

三、Vue2 中的数据劫持实现

1. 对象属性的劫持

Vue 在初始化时会遍历 data 中的所有属性:

function defineReactive(obj, key) {
  let value = obj[key];
  
  Object.defineProperty(obj, key, {
    get() {
      console.log(`读取 ${key}`);
      return value;
    },
    set(newVal) {
      console.log(`更新 ${key}`, newVal);
      value = newVal;
      // 这里会触发视图更新
    }
  });
}

const data = { message: 'Hello Vue' };
defineReactive(data, 'message');

 

2. 嵌套对象的处理

Vue 递归劫持嵌套对象:

function observe(data) {
  if (typeof data !== 'object' || data === null) return;
  
  new Observer(data);
}

class Observer {
  constructor(value) {
    this.value = value;
    this.walk();
  }
  
  walk() {
    Object.keys(this.value).forEach(key => {
      defineReactive(this.value, key);
    });
  }
}

function defineReactive(obj, key) {
  let value = obj[key];
  observe(value); // 递归劫持子属性
  
  Object.defineProperty(obj, key, {
    // getter/setter 略
  });
}
3. 数组的特殊处理

由于 Object.defineProperty 无法检测数组索引变化,Vue 重写了数组方法:

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method];
  
  arrayMethods[method] = function(...args) {
    console.log(`数组执行 ${method} 操作`);
    const result = original.apply(this, args);
    
    // 获取新增的元素
    let inserted;
    switch(method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    
    // 劫持新增元素
    if (inserted) observeArray(inserted);
    
    // 触发视图更新
    return result;
  };
});

function observeArray(items) {
  for (let item of items) {
    observe(item);
  }
}

 

四、结合依赖收集的完整流程

  1. 初始化阶段

    • 遍历 data 属性,通过 defineProperty 设置 getter/setter

    • 递归处理嵌套对象和数组

  2. 依赖收集

    get() {
      if (Dep.target) { // 当前正在计算的 Watcher
        dep.depend();   // 将 Watcher 添加到依赖列表
      }
      return value;
    }

    3.派发更新

    set(newVal) {
      value = newVal;
      dep.notify(); // 通知所有 Watcher 更新
    }

五、数据劫持的局限性

  1. 无法检测属性添加/删除

    this.obj.newProperty = 'value' // 非响应式

    2. 数组索引和长度修改

this.arr[0] = 'new' // 无法检测
this.arr.length = 10 // 无法检测

   3.解决方案

this.$set(this.obj, 'newProperty', 'value')
this.$delete(this.obj, 'oldProperty')

六、Vue3 的改进方案

Vue3 使用 Proxy 替代 Object.defineProperty

  • 可直接监听整个对象而非属性

  • 支持动态添加属性

  • 完美监听数组变化

  • 性能更优

    const proxy = new Proxy(obj, {
      get(target, key) {
        // 拦截读取操作
      },
      set(target, key, value) {
        // 拦截设置操作
      }
    });

总结

Vue2 的数据劫持机制通过 Object.defineProperty 实现,结合依赖收集派发更新,构建了响应式系统的核心。虽然存在一些局限性,但理解其原理有助于我们:

  1. 更好地使用 Vue 的响应式功能

  2. 避免常见的响应式陷阱

  3. 深入理解 Vue 的设计思想

掌握这些原理,能让你在 Vue 开发中更加得心应手,写出更高效、可维护的代码!