Vue 的响应式原理是其核心特性之一,核心目标是实现「数据驱动视图」------ 当数据发生变化时,依赖该数据的视图会自动更新,无需手动操作 DOM。这一机制在 Vue 2 和 Vue 3 中实现方式有较大差异,以下分别详细说明:
Vue 2 的响应式原理(基于 Object.defineProperty
)
Vue 2 采用 Object.defineProperty
对数据进行劫持,结合「发布 - 订阅模式」实现响应式,核心流程包括数据劫持 、依赖收集 和触发更新三个环节。
Vue 2 的响应式系统核心是通过「数据劫持」+「依赖收集」实现数据变化到视图更新的自动触发,整体流程可分为初始化、依赖收集、触发更新三个阶段。
初始化阶段: Vue 会对组件的 data
进行处理。(1)通过 Object.defineProperty
为每个属性添加 get
和 set
拦截器,将普通对象转为响应式对象;(2)同时为每个属性创建对应的 Dep
实例(依赖管理器),用于存储依赖该属性的 Watcher
。
依赖收集阶段: 发生在数据首次被读取时。当组件初始化或 watch
/计算属性触发时,会先实例化 Watcher
(如渲染 Watcher
负责视图更新、watch
的 Watcher
对应回调逻辑),并执行 Watcher
的 get
方法------此时会将 Dep.target
设为当前 Watcher
,再执行 getter
函数(如渲染函数读取 data
属性)。读取属性时触发 get
拦截器,Dep
会通过 Dep.target
识别当前活跃的 Watcher
,将其添加到依赖列表中,完成"数据-Watcher
"的关联。
触发更新阶段: 发生在数据被修改时。当属性值变化,会触发 set
拦截器,此时对应的 Dep
会遍历依赖列表中的所有 Watcher
,调用其 update
方法;Watcher
会通过异步队列(避免频繁更新)触发最终操作,如渲染 Watcher
重新执行渲染函数更新视图,watch
的 Watcher
执行用户定义的回调,从而实现数据变化后视图或逻辑的自动响应。

数据劫持:拦截数据的读写
Object.defineProperty
是 ES5 提供的 API,通过定义对象属性的 get
(读取时触发)和 set
(修改时触发)方法,实现对属性的拦截。Vue 2 会对 data
中的数据递归执行这一操作,使其成为「响应式数据」。
scss
// 递归将对象转为响应式
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return; // 非对象类型无需处理
}
// 遍历对象属性,逐个劫持
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 劫持单个属性
function defineReactive(obj, key, val) {
// 递归处理子对象(如 data 中的嵌套对象)
observe(val);
// 依赖管理器:收集依赖当前属性的订阅者
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 读取属性时,收集依赖(当前活跃的 Watcher)
if (Dep.target) {
dep.addSub(Dep.target); // 将 Watcher 加入依赖列表
}
return val;
},
set(newVal) {
if (newVal === val) return; // 值未变化则不处理
val = newVal;
observe(newVal); // 新值如果是对象,需要递归劫持
// 修改属性时,通知所有依赖更新
dep.notify();
}
});
}
只要访问了响应式对象的属性,就会触发 get()
拦截器 ,无论这种访问是来自模板渲染、watch
、computed
,还是手动代码。get()
的核心作用是在属性被访问时,记录 "谁在访问它"(即 Dep.target
指向的 Watcher
) ,从而建立 "数据→依赖" 的关联,为后续数据变化时的更新通知打下基础。
依赖收集:跟踪使用数据的地方
当响应式数据被读取时(如组件渲染、watch
监听),Vue 需要记录「谁在使用这个数据」(即「依赖」),这一过程称为依赖收集。核心通过 Dep
(依赖管理器)和 Watcher
(订阅者)实现:
Dep
类 :每个响应式属性对应一个 Dep
实例,用于存储依赖该属性的所有 Watcher
。
javascript
class Dep {
static target = null; // 静态属性,指向当前活跃的 Watcher
subs = []; // 存储订阅者(Watcher)
// 添加订阅者
addSub(sub) {
this.subs.push(sub);
}
// 通知所有订阅者更新
notify() {
this.subs.forEach(sub => sub.update());
}
}
Watcher
类 :组件的渲染逻辑、watch
选项、computed
属性等都会被包装成 Watcher
。当依赖的数据变化时,Watcher
会触发更新(如重新渲染组件)。
kotlin
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm; // 当前组件实例
this.cb = cb; // 更新时执行的回调(如重新渲染)
this.getter = expOrFn; // 依赖的表达式或渲染函数
this.get(); // 初始化时触发 get,收集依赖
}
get() {
Dep.target = this; // 标记当前活跃的 Watcher
this.getter.call(this.vm); // 执行渲染函数,触发数据的 get 拦截
Dep.target = null; // 重置,避免重复收集
}
// 数据变化时触发更新
update() {
this.cb(); // 如重新执行渲染函数
}
}
依赖收集过程 :当组件首次渲染时,会执行渲染函数,过程中会读取 data
中的属性,触发 get
拦截器。此时 Dep.target
指向当前组件的 Watcher
,Dep
会将该 Watcher
加入订阅列表,完成依赖收集。
触发更新:数据变化时通知依赖
当修改响应式数据时,会触发 set
拦截器,Dep
会调用 notify()
方法,通知所有订阅的 Watcher
执行 update()
,最终触发组件重新渲染或 watch
回调,实现「数据变 → 视图变」。
Vue 2 响应式的局限性
由于 Object.defineProperty
的设计限制,Vue 2 存在以下问题:
- 无法监听对象新增 / 删除的属性 :只能劫持初始化时已存在的属性,新增属性需通过
this.$set(obj, key, val)
手动触发响应式。 - 无法监听数组的部分操作 :数组的索引修改(如
arr[0] = 1
)、length
变化不会触发set
,因此 Vue 2 重写了数组的 7 个方法(push
、pop
、splice
等)以支持响应式。 - 深层对象递归劫持的性能问题:初始化时需递归劫持所有嵌套对象,数据结构复杂时可能影响性能。
Vue3
Vue 3 的响应式系统基于 Proxy 代理 和 Effect 副作用机制实现,核心是建立"数据变化-副作用执行"的自动关联,流程可分为初始化、依赖收集、触发更新三个阶段。
初始化阶段: 通过 reactive
或 ref
将数据转为响应式对象:reactive
针对对象/数组,使用 Proxy
代理整个对象,拦截 get
(读取)、set
(修改)、deleteProperty
(删除)等操作;ref
针对基本类型,通过封装成带 value
属性的对象,内部同样用 Proxy 代理 value
的读写。同时,每个响应式对象的属性会对应关联的"依赖容器",用于存储依赖该属性的副作用。
依赖收集阶段: 发生在副作用函数执行时。当通过 effect
注册副作用(如组件渲染函数、watch 回调),effect
会先将当前副作用标记为"活跃状态",再执行副作用函数。函数执行中若读取响应式属性,会触发 Proxy 的 get
拦截器:拦截器会定位该属性对应的依赖容器,将活跃副作用添加到容器中,完成"数据-副作用"的关联。
触发更新阶段: 发生在数据被修改时。当修改响应式属性(如赋值、删除),会触发 Proxy 的 set
或 deleteProperty
拦截器:拦截器会先更新数据,再取出该属性依赖容器中的所有副作用,通过调度器(如控制执行时机、避免重复执行)触发副作用重新执行,从而实现数据变化后视图或逻辑的自动响应。 相比 Vue 2 的 Object.defineProperty
,Proxy 能原生支持对象新增属性、数组索引/方法修改等场景,无需额外处理,响应式覆盖更全面。
数据劫持:Proxy 拦截整个对象
Proxy
可以创建一个对象的代理,直接拦截对象的读取、修改、新增、删除等操作,无需逐个拦截属性,支持更全面的响应式劫持。
javascript
// 创建响应式对象
function reactive(target) {
return new Proxy(target, {
// 读取属性时触发(包括对象属性、数组索引、length 等)
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver); // 反射读取,确保正确的 this 指向
// 收集依赖
track(target, key);
// 如果属性值是对象,递归创建代理(懒递归:访问时才处理,优化性能)
if (typeof res === 'object' && res !== null) {
return reactive(res);
}
return res;
},
// 修改/新增属性时触发
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
if (oldValue !== value) {
Reflect.set(target, key, value, receiver);
// 触发更新
trigger(target, key);
}
return true;
},
// 删除属性时触发
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
// 触发更新
trigger(target, key);
}
return result;
}
});
}
依赖收集与触发:track
和 trigger
Vue 3 用 ReactiveEffect
(替代 Vue 2 的 Watcher
)管理依赖,核心逻辑更简洁。
track
(收集依赖) :当读取响应式数据时,记录当前的 ReactiveEffect
与数据的关联。
ini
// 存储依赖:target → key → 依赖集合
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return; // 没有活跃的 effect 则不收集
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); // 将当前活跃的 effect 加入依赖
}
trigger
(触发更新) :当数据变化时,找到所有关联的 ReactiveEffect
并执行其更新逻辑。
ini
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect.run()); // 执行所有依赖的更新
}
}
ReactiveEffect
类:封装副作用函数(如渲染函数),当依赖变化时重新执行。
javascript
let activeEffect = null;
class ReactiveEffect {
constructor(fn) {
this.fn = fn; // 副作用函数(如组件渲染函数)
}
run() {
activeEffect = this; // 标记当前活跃的 effect
this.fn(); // 执行副作用函数(触发数据读取,进而收集依赖)
activeEffect = null; // 重置
}
}
对基本类型的支持:ref
Proxy
只能代理对象,对于基本类型(如 number
、string
),Vue 3 提供 ref
包装。使用时通过 .value
访问,在模板中会自动解包(无需显式写 .value
)。
scss
// 模拟 Vue 内部的 ref 函数
function ref(value) {
// 如果是对象,先转为 reactive(Proxy 代理)
if (isObject(value)) {
value = reactive(value);
}
// 创建 Ref 对象(通过 getter/setter 劫持 value 属性)
const refObject = {
get value() {
track(refObject, 'value'); // 收集依赖
return value;
},
set value(newValue) {
// 如果新值是对象,也需要转为 reactive
if (isObject(newValue)) {
newValue = reactive(newValue);
}
value = newValue;
trigger(refObject, 'value'); // 触发更新
}
};
return refObject;
}
Vue 3 响应式的优势
- 支持对象新增 / 删除属性 :
Proxy
能拦截set
(新增)和deleteProperty
(删除)操作,无需手动调用$set
。 - 原生支持数组响应式 :可监听数组索引修改、
length
变化等,无需重写数组方法。 - 懒递归劫持:嵌套对象只有在被访问时才会创建代理,初始化性能更优。
- 支持复杂数据结构 :如
Map
、Set
等,Proxy
可拦截其set
、delete
等操作。