1、Vue 响应式实现原理
Vue2 利用 Object.defineProperty()
对数据对象的属性进行"劫持"(数据劫持 / 数据代理)。
- 初始化响应式数据
- Vue 在初始化时会遍历
data
中的每个属性,用Object.defineProperty
将它们变成getter/setter
。 - 当你访问或修改这些属性时,会自动触发对应的 getter/setter。
- Vue 在初始化时会遍历
- 依赖收集(Dep + Watcher)
- 每个响应式属性都持有一个 Dep(依赖收集器),用于记录哪些组件/函数依赖了它。
- 当组件渲染时,相关属性的 getter 会被访问,从而将 Watcher 注册到该属性的 Dep 中。
- 派发更新
- 当属性的 setter 被调用(属性变更),会触发 Dep 通知 Watcher 执行更新操作(如重新渲染组件)。
- Dep 就是"依赖记录本"
- 每一个响应式属性(比如 msg)都有一个专属的 Dep 实例
- 它内部维护着一个数组 subs,记录"谁用过我"。
- 这个"谁"指的就是那些依赖当前属性的 Watcher(组件、计算属性、侦听器等)。
- Watcher 是"观察者"或"执行者"
- 它代表"正在运行的视图更新函数"或"计算逻辑"。
- 比如一个组件渲染函数、一个 computed、一个 watcher 函数,它们都是 Watcher。
- 当这些逻辑访问某个响应式数据时,Dep 就把这个 Watcher 收进自己的小本本。
- getter 阶段的行为(收集依赖):
- 就是把当前正在执行的 Watcher 记录到 Dep 中
- setter 阶段的行为(触发更新):
- 修改这个数据时,会触发 dep.subs(里面的所有 Watcher)的执行
扩展:
- Object.defineProperty 只劫持对象,对于基本数据类型不会进行劫持,因为 defineProperty 是对象上的属性;而基本数据类型的数据,会被使用
{name:'张三'}
包裹起来当做一个对象进行劫持,这个对象会挂载到当前组件实例上(vm)。
js
// 只劫持对象
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
- Vue2 中每个.vue 文件会被当做一个组件,在使用时都会被 Vue 实例成一个独立的组件实例(vm),它拥有自己完整的响应式系统、生命周期、作用域,多个组件之间互不影响。
- Vue 的组件实例是一个"树状结构",所有子组件实例最终都会挂载(嵌套)在一个根 Vue 实例 vm 上。
- Vue2 中每个组件内的 this 指向在初始时,都被指向了当前组件实例。
- Vue2 组件里面的 data 要写成函数而不是对象,是为了避免数据被多个实例共享。
- Vue 2 对对象做响应式劫持时,是递归遍历的(深度遍历),确实会劫持所有层级的属性,哪怕你并不一定会用到它们。
js
function defineReactive(obj, key, val) {
observe(val); // 递归:如果 val 是对象,也变成响应式
Object.defineProperty(obj, key, {
get() {
// 做依赖收集
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
observe(newVal); // 如果赋的新值是对象,也递归处理
// 通知依赖更新
}
}
});
}
Object.defineProperty 优点:
- 兼容性极好(ES5 标准)
- Object.defineProperty 是 ES5 标准,支持范围非常广,基本兼容所有主流浏览器(包括 IE9+)
- 这让 Vue 2 在广泛的浏览器环境中都能稳定运行。
- 性能相对较好
- 它是在属性层面劫持,性能开销较低,尤其是对于单层属性的读取和赋值。
- 不像 Proxy,需要代理整个对象,某些场景下 defineProperty 反而更轻量。
- 实现相对直接
- 通过给每个属性定义 getter/setter,劫持数据变化,思想简单直接;
- 代码逻辑清晰,容易理解"数据劫持"的核心机制。
- 内存占用较低:
- 只对已有属性添加访问器,不需要额外代理对象,相对内存消耗更小。
- 细粒度控制
- 你可以针对每个具体属性设置不同的 getter/setter,灵活度高。
Object.defineProperty 缺点:
- 只能劫持"已有属性",不能监听新增或删除的属性;
- 必须递归地遍历所有嵌套属性,成本高、性能差;(嵌套越深、数据越大,初始化越慢,内存占用也越高)
- 无法监听数组索引变动和长度变化(Vue 2 的数组响应式,是对数组原型做"函数重写"(push、pop、splice 等),并在这些方法内部手动触发视图更新。)
- 无法观察对象属性的访问顺序 / 精细控制访问路径
- 代码维护复杂,边界情况多,hack 多(Vue 2 的响应式实现基于 Object.defineProperty,要给对象的每个属性都写一遍 getter/setter,代码逻辑比较繁琐,尤其是要递归遍历嵌套对象,代码量和复杂度都较大。)
Vue 3 响应式原理(基于 Proxy ES6 引入)
- 使用 Proxy 代理整个对象
- 通过 reactive() 方法将整个对象用 Proxy 包装。
- 能够拦截所有操作(get/set/delete 等),更灵活且支持动态属性。
- 依赖收集(effect + targetMap)
- Vue3 用 effect 替代 Vue2 中的 Watcher。
- 所有响应式对象的依赖关系会存储在一个全局的 targetMap 中
- 更新触发
- 当 Proxy 拦截到属性的变更(set),会查找该属性对应的 effect 并执行,实现视图更新。
- Proxy(Vue3)
- 相当于定义的响应式数据,都是被 Proxy 进行包裹过一层;
- 副作用函数(effect)触发渲染,读取的响应式数据;
- 收集依赖就是 Vue 记录下某个响应式数据被哪些副作用函数(effect)用到了
- 触发更新就是当某个响应式数据被修改时,Vue 会找到「依赖这个数据的副作用函数(effect)」,然后执行这些 effect,完成视图或逻辑的更新。
js
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return result;
}
});
}
扩展:
- Proxy 是一个内置的构造函数(类),可以代理整个对象;原生提供的代理功能,只针对一层对象;
- 懒代理(lazy reactive):懒代理(lazy reactive)并不是 Proxy 自身内建的机制,而是 Vue 3 响应式系统基于 Proxy 设计的一个策略。
- Vue 3 的响应式库,在 get 拦截器里检测到访问的属性值是个对象时,会主动给这个值再次包一层 Proxy,实现"按需代理"。
- Vue 3 用 Proxy 的 get 拦截做递归调用,实现深层响应式,且避免了 Vue 2 那种初始化时递归所有层级的性能问题。
Proxy优点:
- 可以监听新增/删除的属性
- 支持数组索引、length
- 无需递归劫持整个对象树(按需代理)
- Vue2 会在一开始 递归遍历整个 data 对象,用 defineProperty 把每个属性都设置getter/setter。
- 初始化性能差(对象大了非常慢)
- 有些从未访问过的字段也被劫持了
- Vue3 是"懒代理",只有当你访问到 state.user.info.address 时,它才会"递归代理"进去,性能提升明显。
- Vue2 会在一开始 递归遍历整个 data 对象,用 defineProperty 把每个属性都设置getter/setter。
- 可以代理整对象,而不是一个个属性
- defineProperty 是属性级别的劫持,得一个个写
- Proxy 是对象级别的代理,一次包住整个对象
- 更强大的拦截能力(支持 13 种操作)
- get、set、deleteProperty、has、ownKeys、getOwnPropertyDescriptor、defineProperty、preventExtensions
- 响应式系统更通用、更简洁
- Vue3 中所有响应式的功能(如 reactive, ref, computed, watchEffect)本质都依赖于 Proxy + effect,设计更加统一简洁易于扩展。
- 更利于 Tree-shaking,支持更小体积打包
- Vue2 的响应式是以 Observer 和 Watcher 类为核心,不容易被 tree-shaking。
- 而 Vue3 的响应式系统更像函数式 reactive, effect, track, trigger 等都是纯函数,支持按需裁剪 ➜ 更小体积。 Proxy缺点:
- 兼容性相对 Vue2 差
- Proxy 是 ES6 的新特性,不支持在旧版浏览器(尤其是 IE11)中运行。
- Vue3 官方明确不支持 IE11,这对某些老项目是个障碍。
- Vue2 使用 Object.defineProperty,可以兼容 IE11。
- 需要全对象代理(不能只代理某个属性)
- Proxy 是代理整个对象,不能"只劫持某个属性"。
- 所以必须用一个"统一的入口"来包裹整个数据对象(比如 reactive()),这会让对部分属性响应式处理变复杂。
- 无法拦截对象内部访问路径
- 比如你访问了 state.user.info.name,你没办法知道这个访问路径是从 state → user → info → name 进来的。
- 带来的问题:无法记录访问路径(不像 MobX 有"路径追踪"能力);也就无法做到某些依赖图精确的性能分析
- 性能可能差于 defineProperty(在一些简单场景),虽然 Proxy 懒劫持大大提升了深层对象初始化性能,但:
- 在某些浅层、结构简单的对象中,defineProperty 手动控制颗粒度更小,开销可能反而更低。
- Proxy 的 handler 执行逻辑会有一定的性能开销(特别是频繁触发 get/set 的场景)。
- WeakMap 缓存不易调试或手动清理
- Vue3 使用了 WeakMap 做响应式对象缓存(防止重复代理):
const reactiveMap = new WeakMap();
- 这虽然高效,但不容易调试(看不到里面内容)
- 手动清理或强制刷新响应式状态比较麻烦
- Vue3 使用了 WeakMap 做响应式对象缓存(防止重复代理):
- 调试难度稍高
- Proxy 的响应式是通过代理对象"偷偷做事"的,一般:
- 数据变了,视图就更新了,但你不知道什么时候 trigger 的
- 这导致新手更难调试响应式问题
- Proxy 的响应式是通过代理对象"偷偷做事"的,一般:
- 某些库对 Proxy 不友好
- 一些第三方库(尤其是老旧库)无法识别 Proxy 对象,会导致 bug。
- 比如序列化、深拷贝、类型判断等可能出错: