Vue响应式进化:从Object.defineProperty到Proxy

Vue3已经发布了一段时间了,那么相比于Vue2做了哪些升级,解决了什么问题呢?

众所周知Vue的响应式系统是其核心特性之一,它能够自动追踪数据变化并更新视图。从Vue 2到Vue 3,响应式系统经历了一次重要的技术升级,从使用Object.defineProperty到采用Proxy,本文将结合关键代码来分析它的进化过程。

Vue 2的响应式实现:Object.defineProperty

基本原理

Vue 2使用Object.defineProperty来实现响应式,其核心原理如下:


/**
 * Vue 2响应式实现的核心代码
 * 主要包含以下几个部分:
 * 1. 数组方法拦截器:重写数组的七个方法
 * 2. 观察者函数:递归观察对象的所有属性
 * 3. 响应式定义:使用Object.defineProperty定义getter/setter
 * 4. 依赖收集器:管理依赖关系
 */

// 1. 创建数组方法拦截器
const oldArrayProto = Array.prototype; // 保存数组原始的原型对象
// 创建新的原型对象,继承自Array.prototype
const newArrayProto = Object.create(oldArrayProto);
// 需要重写的数组方法列表,这些方法会改变原数组,需要特殊处理
const arrayMethods = [
  'push',    // 在数组末尾添加元素
  'pop',     // 删除数组最后一个元素
  'shift',   // 删除数组第一个元素
  'unshift', // 在数组开头添加元素
  'splice',  // 从数组中添加/删除元素
  'sort',    // 对数组进行排序
  'reverse'  // 反转数组
];
// 重写数组方法
arrayMethods.forEach(method => {
  // 保存原始方法
  const original = oldArrayProto[method];
  // 重写方法
  newArrayProto[method] = function(...args) {
    // 调用原始数组方法,保持原有功能
    const result = original.apply(this, args);
    // 获取新增的元素,只有push、unshift、splice这三个方法会新增元素
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        // 这两个方法的所有参数都是新增的元素
        inserted = args;
        break;
      case 'splice':
        // splice方法的第三个参数开始才是新增的元素
        inserted = args.slice(2);
        break;
    }
    // 对新插入的元素进行响应式处理
    if (inserted) {
      observer(inserted);
    }
    // 触发更新
    console.log('数组更新了');
    return result;
  };
});

/**
 * 2. 观察者函数
 * 用于递归观察对象的所有属性
 * @param {Object|Array} data - 需要观察的数据
 * @returns {Object|Array} - 返回观察后的数据
 */
function observer(data) {
  // 如果不是对象或者是null,直接返回
  if (typeof data !== 'object' || data === null) {
    return data;
  }
  // 如果是数组,重写数组的原型
  if (Array.isArray(data)) {
    // 将数组的原型指向我们重写后的原型
    data.__proto__ = newArrayProto;
    // 对数组中的每个元素进行观察
    data.forEach(item => observer(item));
    return;
  }
  // 遍历对象的所有属性
  for (let key in data) {
    defineReactive(data, key, data[key]);
  }
}

/**
 * 3. 定义响应式
 * 使用Object.defineProperty定义getter/setter
 * @param {Object} target - 目标对象
 * @param {string} key - 属性名
 * @param {any} value - 属性值
 */
function defineReactive(target, key, value) {
  // 递归观察value,处理嵌套对象
  observer(value);
  // 为每个属性创建依赖收集器
  const dep = new Dep();
  Object.defineProperty(target, key, {
    get() {
      // 依赖收集,当有watcher访问这个属性时,将watcher添加到依赖中
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set(newValue) {
      // 如果新值和旧值相同,不做处理
      if (newValue === value) return;
      // 对新值进行观察,处理新值是对象的情况
      observer(newValue);
      value = newValue;
      // 通知所有依赖进行更新
      dep.notify();
    }
  });
}

/**
 * 4. 依赖收集器
 * 用于管理依赖关系,实现发布订阅模式
 */
class Dep {
  constructor() {
    // 使用Set存储依赖,避免重复
    this.subscribers = new Set();
  }
  // 添加依赖
  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }
  // 通知所有依赖进行更新
  notify() {
    this.subscribers.forEach(watcher => watcher.update());
  }
}

// 5. 使用示例
let data = {
  name: 'zdx',
  age: 26,
  address: {
    city: 'hangzhou',
    area: 'xihu'
  },
  hobbies: ['reading', 'coding']
};

// 将数据转换为响应式
observer(data);

局限性

  1. 对象属性监听限制

    • 无法检测对象属性的添加或删除
    • 需要使用Vue.setthis.$set来添加新属性
    • 需要使用Vue.deletethis.$delete来删除属性
  2. 数组操作限制

    • 无法直接监听数组索引和长度的变化
    • 需要重写数组方法(push、pop、shift、unshift、splice、sort、reverse)
  3. 性能开销

    • 需要递归遍历对象的所有属性
    • 每个属性都需要单独的getter/setter

Vue 3的响应式实现:Proxy

基本原理

Vue 3使用Proxy来实现响应式,其核心原理如下:


/**
 * Vue 3响应式实现的核心代码
 * 主要包含以下几个部分:
 * 1. 响应式对象创建:使用Proxy创建响应式对象
 * 2. 依赖收集:使用WeakMap存储依赖关系
 * 3. 触发更新:通知依赖进行更新
 * 4. 副作用函数:处理副作用
 */

/**
 * 1. 创建响应式对象
 * 使用Proxy创建响应式对象
 * @param {Object} target - 目标对象
 * @returns {Proxy} - 返回响应式对象
 */
function reactive(target = {}) {
  // 如果不是对象或者是null,直接返回
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  // 如果已经是响应式对象,直接返回,避免重复代理
  if (target.__isReactive) {
    return target;
  }
  // Proxy的处理器对象
  const proxyConfig = {
    /**
     * 拦截对象属性的读取操作
     * @param {Object} target - 目标对象
     * @param {string} key - 属性名
     * @param {Object} receiver - Proxy实例
     */
    get(target, key, receiver) {
      // 获取原始值
      const result = Reflect.get(target, key, receiver);
      // 依赖收集
      track(target, key);
      // 递归处理嵌套对象
      return reactive(result);
    },
    /**
     * 拦截对象属性的设置操作
     * @param {Object} target - 目标对象
     * @param {string} key - 属性名
     * @param {any} value - 新值
     * @param {Object} receiver - Proxy实例
     */
    set(target, key, value, receiver) {
      // 获取旧值
      const oldValue = target[key];
      // 设置新值
      const result = Reflect.set(target, key, value, receiver);
      // 如果值发生变化,触发更新
      if (oldValue !== value) {
        trigger(target, key);
      }
      return result;
    },
    /**
     * 拦截对象属性的删除操作
     * @param {Object} target - 目标对象
     * @param {string} key - 属性名
     */
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      if (result) {
        trigger(target, key);
      }
      return result;
    }
  };
  // 创建Proxy实例
  const observed = new Proxy(target, proxyConfig);
  // 标记为响应式对象
  observed.__isReactive = true;
  return observed;
}

/**
 * 2. 依赖收集
 * 使用WeakMap存储依赖关系
 * 结构:WeakMap -> Map -> Set
 * WeakMap: target -> Map
 * Map: key -> Set
 * Set: effect functions
 */
const targetMap = new WeakMap();
/**
 * 依赖收集函数
 * @param {Object} target - 目标对象
 * @param {string} key - 属性名
 */
function track(target, key) {
  // 如果没有活动的副作用函数,直接返回
  if (!activeEffect) return;
  // 获取target对应的依赖Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  // 获取key对应的依赖Set
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  // 将当前活动的副作用函数添加到依赖中
  dep.add(activeEffect);
}

/**
 * 3. 触发更新
 * 通知依赖进行更新
 * @param {Object} target - 目标对象
 * @param {string} key - 属性名
 */
function trigger(target, key) {
  // 获取target对应的依赖Map
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  // 获取key对应的依赖Set
  const dep = depsMap.get(key);
  if (dep) {
    // 执行所有依赖的副作用函数
    dep.forEach(effect => effect());
  }
}

/**
 * 4. 副作用函数,用于处理副作用
 */
let activeEffect = null;
/**
 * 创建副作用函数
 * @param {Function} fn - 副作用函数
 */
function effect(fn) {
  // 保存当前活动的副作用函数
  activeEffect = fn;
  // 执行副作用函数
  fn();
  // 清除当前活动的副作用函数
  activeEffect = null;
}

// 5. 使用示例
const data = reactive({
  name: 'zdx',
  age: 26,
  info: {
    city: 'hangzhou',
    detail: {
      address: 'xihu'
    }
  },
  hobbies: ['reading', 'coding']
});

// 监听数据变化
effect(() => {
  console.log('数据更新:', data.name);
});

// 修改数据
data.name = 'zdx1';
data.info.city = 'shanghai';
data.hobbies.push('gaming');

优势

  1. 更强大的拦截能力

    • 可以监听对象属性的添加和删除
    • 可以监听数组索引和长度的变化
    • 可以监听Map、Set、WeakMap、WeakSet等数据结构
  2. 更好的性能

    • 不需要递归遍历对象的所有属性
    • 按需响应,只有在访问属性时才会进行依赖收集
  3. 更简洁的API

    • 不需要特殊的API(如Vue.set)来添加或删除属性
    • 可以直接使用原生JavaScript操作对象

实际应用示例

Vue 2中的响应式

// Vue 2
const vm = new Vue({
  data: {
    user: {
      name: 'John'
    }
  }
})
// 需要特殊API
vm.$set(vm.user, 'age', 25)
vm.$delete(vm.user, 'name')

Vue 3中的响应式

// Vue 3
const state = reactive({
  user: {
    name: 'John'
  }
})
// 直接操作即可
state.user.age = 25
delete state.user.name

实现响应式的技术演进

  • Vue 2:Object.defineProperty,Vue 3:Proxy
  • 主要改进:更强大的数据监听能力和性能表现
  • 注意Proxy的浏览器兼容性要求
posted @ 2025-06-11 16:26  Justus-  阅读(54)  评论(0)    收藏  举报