返回首页

Vue3.0 为什么要用 Proxy API 替代 defineProperty API?

问题解析

这是 Vue3 最核心的底层变化之一。面试考察这个问题,是想要了解候选人对响应式原理的深入理解,以及能否从技术原理层面分析两种方案的优劣。

核心概念

两种响应式方案对比

┌─────────────────────────────────────────────────────┐
│           响应式方案对比                             │
├───────────────────────────────────┬─────────────────┤
│      Vue2 defineProperty          │   Vue3 Proxy    │
├───────────────────────────────────┼─────────────────┤
│ 遍历对象属性逐一劫持              │ 直接代理整个对象 │
│ 无法监听新增/删除属性             │ 自动监听新增/删除 │
│ 无法监听数组索引变化              │ 自动监听数组索引  │
│ 需要重写数组方法                  │ 原生支持数组方法  │
│ 嵌套对象需要深度递归              │ 懒递归,访问时才代理 │
│ 兼容 IE9+                         │ 不兼容 IE,无 polyfill │
└───────────────────────────────────┴─────────────────┘

详细解答

一、Object.defineProperty 详解

1. 基本实现原理

function defineReactive(obj, key, val) {
  // 递归处理嵌套对象
  observe(val);

  const dep = new Dep();  // 依赖收集器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 依赖收集
      if (Dep.target) {
        dep.depend();
      }
      console.log(`获取 ${key}: ${val}`);
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      console.log(`设置 ${key}: ${newVal}`);
      val = newVal;
      // 递归处理新值
      observe(newVal);
      // 触发更新
      dep.notify();
    }
  });
}

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

  // 遍历所有属性进行劫持
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}

// 使用
const data = { name: 'Tom', age: 20 };
observe(data);

data.name;      // 获取 name: Tom
data.name = 'Jerry';  // 设置 name: Jerry
data.age++;     // 获取 age: 20, 设置 age: 21

2. 存在的问题

问题1:无法监听新增属性

const data = { name: 'Tom' };
observe(data);

// 新增属性
data.age = 20;  // ❌ 无法监听,不会触发更新

// Vue2 解决方案
Vue.set(data, 'age', 20);  // 需要显式调用

问题2:无法监听删除属性

const data = { name: 'Tom', age: 20 };
observe(data);

// 删除属性
delete data.age;  // ❌ 无法监听,不会触发更新

// Vue2 解决方案
Vue.delete(data, 'age');  // 需要显式调用

问题3:数组问题

const arr = [1, 2, 3];
observe(arr);

// 通过索引修改
arr[0] = 100;     // ✅ 可以监听(如果已存在索引)

// 通过索引新增
arr[3] = 4;       // ❌ 无法监听

// 修改数组长度
arr.length = 10;  // ❌ 无法监听

// 数组方法
arr.push(5);      // ❌ 无法监听(需要重写方法)
arr.pop();        // ❌ 无法监听

Vue2 的数组解决方案 - 重写数组方法:

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

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method];

  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      const result = original.apply(this, args);
      const ob = this.__ob__;

      // 插入的元素也需要被监听
      let inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break;
        case 'splice':
          inserted = args.slice(2);
          break;
      }
      if (inserted) ob.observeArray(inserted);

      // 通知更新
      ob.dep.notify();
      return result;
    },
    enumerable: false,
    writable: true,
    configurable: true
  });
});

// 替换数组原型
function observeArray(items) {
  for (let i = 0; i < items.length; i++) {
    observe(items[i]);
  }
}

问题4:深度递归性能问题

const data = {
  a: {
    b: {
      c: {
        d: 1  // 嵌套层级深
      }
    }
  }
};

// Vue2: 初始化时需要深度递归所有属性
observe(data);
// 需要递归: a -> b -> c -> d
// 即使某些属性永远不会被访问,也会被监听

二、Proxy 详解

1. 基本实现原理

function reactive(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      console.log(`获取 ${key}: ${res}`);

      // 依赖收集
      track(target, key);

      // 懒递归:访问时才建立响应式
      return isObject(res) ? reactive(res) : res;
    },

    set(target, key, value, receiver) {
      const hadKey = hasOwn(target, key);
      const oldValue = target[key];
      const res = Reflect.set(target, key, value, receiver);

      if (!hadKey) {
        // 新增属性
        console.log(`新增 ${key}: ${value}`);
        trigger(target, key, 'add');
      } else if (hasChanged(value, oldValue)) {
        // 修改属性
        console.log(`修改 ${key}: ${value}`);
        trigger(target, key, 'set');
      }

      return res;
    },

    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key);
      const res = Reflect.deleteProperty(target, key);

      if (hadKey) {
        // 删除属性
        console.log(`删除 ${key}`);
        trigger(target, key, 'delete');
      }

      return res;
    },

    // 更多拦截器
    has(target, key) {
      // in 操作符
      const res = Reflect.has(target, key);
      track(target, key);
      return res;
    },

    ownKeys(target) {
      // Object.keys, for...in
      track(target, ITERATE_KEY);
      return Reflect.ownKeys(target);
    }
  });

  return observed;
}

2. 解决 defineProperty 的所有问题

解决新增/删除属性问题:

const state = reactive({ name: 'Tom' });

// 新增属性
state.age = 20;      // ✅ 自动监听,触发 'add' 类型更新

// 删除属性
delete state.name;   // ✅ 自动监听,触发 'delete' 类型更新

解决数组问题:

const arr = reactive([1, 2, 3]);

// 通过索引修改
arr[0] = 100;        // ✅ 自动监听

// 通过索引新增
arr[3] = 4;          // ✅ 自动监听

// 修改数组长度
arr.length = 10;     // ✅ 自动监听

// 数组方法
arr.push(5);         // ✅ 原生支持
arr.pop();           // ✅ 原生支持
arr.splice(1, 1);    // ✅ 原生支持

解决性能问题 - 懒递归:

const data = reactive({
  a: {
    b: {
      c: {
        d: 1
      }
    }
  }
});

// 初始化时:只代理最外层对象,不会递归
// 访问 data.a 时:才对 a 建立响应式
// 访问 data.a.b 时:才对 b 建立响应式
// 以此类推...

console.log(data.a.b.c.d);  // 逐层建立响应式

3. Proxy 的其他优势

支持更多拦截器:

const proxy = new Proxy(obj, {
  get(target, key, receiver) {},        // 读取属性
  set(target, key, value, receiver) {}, // 设置属性
  deleteProperty(target, key) {},       // 删除属性
  has(target, key) {},                  // in 操作符
  ownKeys(target) {},                   // Object.keys, for...in
  getOwnPropertyDescriptor(target, key) {},
  defineProperty(target, key, descriptor) {},
  preventExtensions(target) {},         // Object.preventExtensions
  getPrototypeOf(target) {},            // Object.getPrototypeOf
  setPrototypeOf(target, proto) {},     // Object.setPrototypeOf
  apply(target, thisArg, args) {},      // 函数调用
  construct(target, args, newTarget) {} // new 操作符
});

支持 Map/Set/WeakMap/WeakSet:

const map = reactive(new Map());

map.set('key', 'value');  // ✅ 可以监听
map.get('key');           // ✅ 可以监听
map.has('key');           // ✅ 可以监听
map.delete('key');        // ✅ 可以监听
map.clear();              // ✅ 可以监听

const set = reactive(new Set());

set.add(1);               // ✅ 可以监听
set.has(1);               // ✅ 可以监听
set.delete(1);            // ✅ 可以监听

三、完整对比

特性 defineProperty Proxy
拦截目标 单个属性 整个对象
新增属性 ❌ 不支持 ✅ 支持
删除属性 ❌ 不支持 ✅ 支持
数组索引 ❌ 不支持 ✅ 支持
数组长度 ❌ 不支持 ✅ 支持
Map/Set ❌ 不支持 ✅ 支持
初始化方式 深度递归 懒递归
IE 兼容 IE9+ ❌ 不支持

深入理解

1. 依赖收集的优化

Vue2 的依赖收集:

// 每个属性有自己的 Dep
data: {
  name: 'Tom',  // Dep1
  age: 20,      // Dep2
  info: {       // Dep3
    address: 'Beijing'  // Dep4
  }
}
// 4 个 Dep 实例

Vue3 的依赖收集:

// 使用 WeakMap 存储依赖关系
const targetMap = new WeakMap();

// 结构:
// WeakMap {
//   target(obj) -> Map {
//     key -> Set [effect1, effect2]
//   }
// }

track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, depsMap = new Map());
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, dep = new Set());
  }
  dep.add(activeEffect);
}

2. Vue3 响应式系统的分层设计

┌─────────────────────────────────────────┐
│  应用层 (Vue Component)                  │
├─────────────────────────────────────────┤
│  运行时核心 (runtime-core)               │
│  • 组件实例管理                          │
│  • 生命周期                              │
│  • 调度器 (Scheduler)                    │
├─────────────────────────────────────────┤
│  响应式系统 (reactivity)                 │
│  • ref / reactive / readonly             │
│  • computed / watch / watchEffect        │
│  • effect 副作用管理                      │
├─────────────────────────────────────────┤
│  底层实现                                │
│  • Proxy 代理                            │
│  • Reflect 反射                          │
│  • WeakMap 依赖存储                       │
└─────────────────────────────────────────┘

3. ref vs reactive 的实现差异

// ref - 对基本类型的包装
function ref(value) {
  return createRef(value, false);
}

function createRef(rawValue, shallow) {
  if (isRef(rawValue)) {
    return rawValue;
  }

  return new RefImpl(rawValue, shallow);
}

class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow;
    this._value = __v_isShallow ? value : toReactive(value);
    this._rawValue = value;
  }

  get value() {
    trackRefValue(this);
    return this._value;
  }

  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = this.__v_isShallow ? newVal : toReactive(newVal);
      triggerRefValue(this);
    }
  }
}

// reactive - 对象的响应式代理
function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,      // Proxy handlers
    mutableCollectionHandlers
  );
}

最佳实践

1. 选择合适的响应式 API

import { ref, reactive, shallowRef, shallowReactive, readonly } from 'vue';

// 基本类型 - 使用 ref
const count = ref(0);
const name = ref('Tom');
const isActive = ref(false);

// 对象类型 - 使用 reactive
const state = reactive({
  user: { name: 'Tom', age: 20 },
  list: []
});

// 大型数据 - 使用 shallowRef/shallowReactive
const bigList = shallowRef([
  { id: 1, /* ... */ },
  { id: 2, /* ... */ },
  // 10000 条数据
]);

// 只读数据 - 使用 readonly
const config = readonly({
  apiUrl: 'https://api.example.com',
  timeout: 5000
});

2. 避免解构丢失响应性

const state = reactive({ count: 0, name: 'Tom' });

// ❌ 错误:解构会丢失响应性
const { count, name } = state;
count++;  // 不会触发更新

// ✅ 正确:使用 toRefs
import { toRefs } from 'vue';
const { count, name } = toRefs(state);
count.value++;  // 会触发更新

// ✅ 正确:在模板中直接使用
// {{ state.count }} 会保持响应性

3. 数组和集合的正确使用

const state = reactive({
  list: []
});

// ✅ 正确:使用数组方法
state.list.push(item);
state.list.splice(index, 1);
state.list.sort((a, b) => a - b);

// ✅ 正确:直接替换整个数组
state.list = newList;

// ✅ 正确:使用响应式 Map/Set
const map = reactive(new Map());
map.set('key', value);

const set = reactive(new Set());
set.add(value);

面试要点

  1. 能够清晰解释 defineProperty 的缺陷

    • 无法监听新增/删除属性
    • 无法监听数组索引和长度变化
    • 初始化时需要深度递归
  2. 理解 Proxy 的优势

    • 直接代理整个对象
    • 自动监听新增/删除属性
    • 原生支持数组操作
    • 懒递归,性能更好
  3. 了解 Vue2 的解决方案

    • Vue.set / Vue.delete
    • 重写数组方法
    • 这些方案增加了心智负担
  4. 理解懒递归的优化

    • 访问时才建立深层响应式
    • 减少了初始化开销
  5. 知道 Proxy 的兼容性限制

    • 不兼容 IE11
    • 无法 polyfill

核心结论

  • Proxy 解决了 defineProperty 的所有已知问题
  • Proxy 提供了更强大和灵活的拦截能力
  • Vue3 的响应式系统更加完善和高效
  • 代价是不再支持 IE,需要权衡