Vue 响应式原理:从 2.x 的 Object.defineProperty 到 3.x 的 Proxy

一张图记住结论:
Vue2 = 递归属性劫持defineProperty
Vue3 = 整体对象代理Proxy


1. 为什么要变?

痛点 Vue2 表现 Vue3 解决
新增/删除属性 不能自动触发更新 自动追踪
数组下标/length 需要重写 7 个变异方法 原生语法直接拦截
深层对象 初始化时一次性递归,耗时 用到哪层代理哪层(懒代理)
IE 兼容 必须支持 IE9 只支持现代浏览器

2. Vue2:Object.defineProperty 时代

2.1 核心思路

  1. 遍历 data 所有属性
  2. 为每个属性调用 Object.defineProperty 植入 getter/setter
  3. getter 中收集依赖(Dep.target = Watcher
  4. setter 中派发更新(dep.notify()

2.2 最小实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function defineReactive(obj, key, val) {
const dep = new Dep(); // 每属性一个管家
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.depend(); // 收集 Watcher
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 通知所有 Watcher
}
});
}

2.3 缺陷

  • 属性必须在初始化时就存在,否则需要 Vue.set
  • 数组索引直接修改无法追踪
  • 深层对象一次性递归,启动慢

3. Vue3:Proxy 时代

3.1 核心思路

  1. 不再操作属性,而是代理整个对象
  2. new Proxy(target, handler) 拦截 13 种操作
  3. 依赖收集用 WeakMap + Map 做全局缓存(targetMap
  4. 懒代理:只有访问到深层对象时才返回新的 Proxy

3.2 最小实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const targetMap = new WeakMap();

function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 收集
return isObject(res) ? reactive(res) : res; // 懒递归
},
set(target, key, val, receiver) {
const oldVal = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, val, receiver);
if (!hadKey) trigger(target, key, 'add');
else if (oldVal !== val) trigger(target, key, 'set');
return result;
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey) trigger(target, key, 'delete');
return result;
}
});
}

3.3 优势

  • 新增/删除属性自动追踪,无需 Vue.set
  • 数组索引、length 直接拦截
  • 启动快,内存占用低
  • 可拦截 infor...inObject.keys 等 13 种操作

4. 一张脑图总结

1
2
3
4
5
6
7
8
9
10
11
12
13
Vue2 响应式
├─ 入口:new Vue() → initState() → initData()
├─ 遍历 data → observe(obj) → new Observer()
├─ 为每个属性 defineReactive → getter/setter
├─ Dep 管理依赖,Watcher 订阅
└─ 缺陷:新增属性、数组索引、深层递归

Vue3 响应式
├─ 入口:createApp() → setup() → reactive()
├─ 一次性 new Proxy() 代理根对象
├─ track() 收集,trigger() 派发
├─ WeakMap 全局缓存,懒代理
└─ 解决 Vue2 所有盲区,性能更好

5. 面试一句话回答

“Vue2 通过 Object.defineProperty 递归属性实现数据劫持,新增/删除属性无法自动响应;
Vue3 改用 Proxy 整体代理对象,能拦截 13 种操作,新增/删除属性、数组索引均可自动追踪,且采用懒代理策略,启动更快。”


6. 延伸阅读