
引言:为什么响应式是 Vue 的 "灵魂"?
作为 Vue 开发者,你一定对这样的场景习以为常:
- 修改
data里的变量,页面 DOM 自动同步更新;- 表单输入时,绑定的响应式数据实时变化;
- 数组
push新元素后,列表自动渲染新增项。
这背后的 "魔法",正是 Vue 的响应式系统------ 它让数据与视图建立 "自动关联",开发者无需手动操作 DOM,只需关注数据逻辑,极大提升开发效率。
但你是否好奇:
- 数据变化时,Vue 是如何 "感知" 到的?
- 为什么 Vue2 中给对象新增属性需要
Vue.set,而 Vue3 不需要?ref和reactive到底该怎么选?它们的底层逻辑是什么?
本文将从 "原理拆解→版本对比→实战应用→避坑指南" 四个维度,彻底讲透 Vue 响应式的核心逻辑:先剖析 Vue2 的Object.defineProperty实现,再详解 Vue3 的Proxy升级方案,最后结合ref/reactive的实战场景,让你不仅 "会用",更能 "懂原理"。
1. 响应式的核心本质:数据劫持 + 依赖收集
在深入版本差异前,我们先明确响应式的核心目标:当数据发生变化时,自动触发依赖该数据的视图更新或逻辑执行。
要实现这个目标,需要解决两个关键问题:
- 数据劫持:如何 "监听" 数据的读写操作(比如修改属性值、数组新增元素)?
- 依赖收集:如何记录 "哪些视图 / 逻辑依赖了这个数据"?
这就像一个 "订阅 - 发布" 系统:
- 数据是 "发布者",视图 / 逻辑是 "订阅者";
- 数据劫持负责 "监听发布者的动作"(比如数据被修改);
- 依赖收集负责 "记录订阅者列表";
- 当数据变化时,发布者通知所有订阅者执行更新。

Vue2 和 Vue3 的响应式系统,本质上都是围绕这两个核心问题展开,只是数据劫持的实现方式不同------ 这也是两者差异的根源。
2. Vue2 响应式原理:Object.defineProperty 的 "功与过"
Vue2 的响应式核心是Object.defineProperty(以下简称 "defineProperty"),这是 ES5 提供的 API,用于劫持对象的单个属性,实现对属性读写的监听。
2.1 核心原理:劫持属性 + Dep/Watcher 机制
2.1.1 第一步:用 defineProperty 劫持对象属性
Vue2 会遍历data中的所有对象属性,通过defineProperty重写getter和setter方法:
getter:当属性被访问时触发,用于 "收集依赖"(记录哪些地方用到了这个属性);setter:当属性被修改时触发,用于 "派发更新"(通知所有依赖该属性的地方执行更新)。
2.1.2 第二步:Dep(依赖收集容器)
每个被劫持的属性,都会对应一个Dep实例(可以理解为 "订阅者列表"):
Dep有addSub方法:将依赖(Watcher 实例)加入列表;Dep有notify方法:遍历列表,通知所有 Watcher 执行更新。
2.1.3 第三步:Watcher(依赖订阅者)
Watcher是 "依赖的具体载体",每个视图组件、计算属性、watch监听,都会对应一个Watcher实例:
- 当组件渲染时,会触发属性的
getter,将当前Watcher加入属性的Dep列表;- 当属性被修改时,触发
setter,调用Dep.notify(),所有Watcher会执行update方法,触发组件重新渲染或watch回调。
2.2 Vue2 响应式完整流程

2.3 代码示例:手动实现 Vue2 响应式核心
javascript
// 1. 依赖收集容器:Dep
class Dep {
constructor() {
this.subscribers = []; // 存储依赖(Watcher实例)
}
// 添加依赖
addSub(watcher) {
this.subscribers.push(watcher);
}
// 通知所有依赖更新
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}
// 2. 订阅者:Watcher
class Watcher {
constructor(updateFn) {
this.updateFn = updateFn; // 依赖更新时执行的函数
}
// 执行更新
update() {
this.updateFn();
}
}
// 3. 响应式处理函数:劫持对象属性
function observe(obj) {
// 只处理对象类型
if (typeof obj !== 'object' || obj === null) return;
// 遍历对象所有属性
Object.keys(obj).forEach(key => {
let value = obj[key];
const dep = new Dep(); // 每个属性对应一个Dep
// 重写getter/setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 访问属性时,收集依赖(此时Watcher已被激活)
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
// 修改属性时,通知依赖更新
dep.notify();
}
});
// 递归处理嵌套对象
observe(value);
});
}
// 4. 测试:模拟Vue2响应式
const data = { name: 'Vue2', age: 8 };
observe(data);
// 创建Watcher(模拟组件渲染逻辑)
Dep.target = new Watcher(() => {
console.log(`视图更新:name=${data.name}, age=${data.age}`);
});
// 首次访问属性,触发getter,收集依赖
console.log(data.name); // 触发getter,Dep收集Watcher
Dep.target = null;
// 修改属性,触发setter,通知更新
data.name = 'Vue2响应式'; // 输出:视图更新:name=Vue2响应式, age=8
data.age = 9; // 输出:视图更新:name=Vue2响应式, age=9
2.4 Vue2 响应式的 "致命缺陷"(实战痛点)
虽然defineProperty实现了响应式核心,但在实际开发中,它的设计限制带来了诸多痛点:
2.4.1 1. 无法监听对象新增 / 删除属性
defineProperty只能劫持已存在的属性 ,如果给对象新增属性(比如data.user = { name: '张三' }),或删除属性(delete data.name),不会触发getter/setter,导致响应式失效。
javascript
// Vue2中新增属性,视图不更新
const vm = new Vue({
data() {
return { user: { age: 20 } };
}
});
vm.user.name = '张三'; // 新增属性,视图不更新
delete vm.user.age; // 删除属性,视图不更新
// 解决方案:使用Vue.set/Vue.delete
this.$set(this.user, 'name', '张三');
this.$delete(this.user, 'age');
2.4.2 2. 无法监听数组的 "部分操作"
Vue2 对数组的push、pop、shift、unshift、splice、sort、reverse7 种方法进行了 "重写",可以触发响应式。但以下场景仍无法监听:
- 直接修改数组索引:
arr[0] = 10;- 直接修改数组长度:
arr.length = 0。
javascript
// Vue2中数组操作的坑
const vm = new Vue({
data() {
return { list: [1, 2, 3] };
}
});
vm.list[0] = 10; // 直接修改索引,视图不更新
vm.list.length = 0; // 直接修改长度,视图不更新
// 解决方案:用重写的方法或Vue.set
vm.list.splice(0, 1, 10); // 可用
this.$set(vm.list, 0, 10); // 可用
2.4.3 3. 嵌套对象需要递归劫持,性能开销大
Vue2 需要遍历对象的所有属性,并且对嵌套对象进行递归劫持(比如data.user.address.city)。如果对象层级过深、属性过多,会导致初始化时的性能损耗。
2.4.4 4. 只能劫持对象,无法劫持原始值
defineProperty只能作用于对象的属性 ,无法直接劫持number、string、boolean等原始值。Vue2 通过将原始值包装在data对象中(比如data: { count: 0 })间接实现响应式。
3. Vue3 响应式原理:Proxy 的 "全方位升级"
为了解决 Vue2 的痛点,Vue3 放弃了defineProperty,转而采用 ES6 的Proxy(代理)API,配合Reflect(反射),实现了更强大、更灵活的响应式系统。
3.1 核心原理:代理对象 + effect 依赖收集
3.1.1 第一步:Proxy 代理整个对象
Proxy的核心优势是直接代理整个对象,而非单个属性:
- 无需遍历对象属性(初始化性能更优);
- 能监听对象的所有操作(新增属性、删除属性、数组索引修改等);
- 支持嵌套对象的自动代理(访问嵌套属性时才递归代理,懒加载模式)。
Proxy的常用拦截器(针对响应式):
get(target, key):拦截属性访问(对应 Vue2 的getter,用于收集依赖);set(target, key, value):拦截属性修改 / 新增(对应 Vue2 的setter,用于派发更新);deleteProperty(target, key):拦截属性删除(Vue2 无法监听)。
3.1.2 第二步:Reflect 反射 API
Reflect是 ES6 提供的内置对象,用于 "反射" 对象的操作,与Proxy配合使用:
- 替代
Object的方法(比如Reflect.get(target, key)等价于target[key]);- 统一返回操作结果(比如
Reflect.set返回boolean,表示修改是否成功);- 避免直接操作目标对象,更安全、更规范。
3.1.3 第三步:effect(依赖收集核心)
Vue3 用effect函数替代了 Vue2 的Watcher,核心作用是:
- 执行一个函数(比如组件渲染函数、
watch回调);- 在函数执行过程中,收集依赖(记录哪些响应式数据被访问);
- 当响应式数据变化时,重新执行这个函数。
3.2 Vue3 响应式完整流程

3.3 代码示例:手动实现 Vue3 响应式核心
javascript
// 1. 依赖收集:存储target(目标对象)→ key(属性)→ effects(依赖集合)
const targetMap = new WeakMap();
// 2. 收集依赖:将effect加入对应key的依赖集合
function track(target, key) {
// 没有当前effect,直接返回
if (!activeEffect) return;
// 目标对象的依赖映射(target → Map(key → Set(effects)))
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
// 属性的依赖集合(key → Set(effects))
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
// 将当前effect加入依赖集合
dep.add(activeEffect);
}
// 3. 派发更新:执行所有依赖的effect
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
// 执行所有effect
dep.forEach(effect => effect());
}
}
// 4. 全局变量:存储当前活跃的effect
let activeEffect = null;
// 5. 依赖函数:effect
function effect(fn) {
// 包装effect函数
const effectFn = () => {
activeEffect = effectFn; // 激活当前effect
fn(); // 执行回调,触发属性访问,收集依赖
activeEffect = null; // 重置
};
effectFn(); // 首次执行
return effectFn;
}
// 6. 响应式核心:reactive(基于Proxy)
function reactive(target) {
// 只处理对象/数组类型
if (typeof target !== 'object' || target === null) return target;
// 创建Proxy代理
return new Proxy(target, {
// 拦截属性访问
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
// 递归代理嵌套对象(懒加载)
return reactive(result);
},
// 拦截属性修改/新增
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const success = Reflect.set(target, key, value, receiver);
// 只有值变化时才触发更新
if (success && oldValue !== value) {
trigger(target, key); // 派发更新
}
return success;
},
// 拦截属性删除
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
if (success) {
trigger(target, key); // 派发更新
}
return success;
}
});
}
// 7. 测试:模拟Vue3响应式
const data = reactive({ name: 'Vue3', age: 5, list: [1, 2, 3] });
// 创建effect(模拟组件渲染)
effect(() => {
console.log(`视图更新:name=${data.name}, age=${data.age}, list=[${data.list}]`);
});
// 1. 修改已存在属性
data.name = 'Vue3响应式'; // 输出:视图更新:name=Vue3响应式, age=5, list=[1,2,3]
// 2. 新增属性(Vue2不支持)
data.gender = 'male'; // 输出:视图更新:name=Vue3响应式, age=5, list=[1,2,3](gender默认undefined)
// 3. 删除属性(Vue2不支持)
delete data.age; // 输出:视图更新:name=Vue3响应式, age=undefined, list=[1,2,3]
// 4. 修改数组索引(Vue2不支持)
data.list[0] = 10; // 输出:视图更新:name=Vue3响应式, age=undefined, list=[10,2,3]
// 5. 修改数组长度(Vue2不支持)
data.list.length = 2; // 输出:视图更新:name=Vue3响应式, age=undefined, list=[10,2]
3.4 Vue3 响应式的 "核心优势"(解决 Vue2 痛点)
3.4.1 1. 支持对象新增 / 删除属性
Proxy的set拦截器能监听属性新增,deleteProperty能监听属性删除,无需Vue.set/Vue.delete,自然支持响应式。
3.4.2 2. 完美支持数组所有操作
无论是修改数组索引、长度,还是使用数组方法,Proxy都能拦截,彻底解决 Vue2 的数组监听痛点。
3.4.3 3. 嵌套对象懒加载代理
Vue3 不会在初始化时递归劫持所有嵌套对象,而是在访问嵌套属性时才进行代理 (比如data.user.address,只有访问address时才代理它),初始化性能大幅提升。
3.4.4 4. 支持更多数据类型
除了对象 / 数组,Proxy还能代理Map、Set等集合类型(Vue2 不支持),响应式覆盖范围更广。
3.4.5 5. 更简洁的代码结构
无需遍历对象属性,Proxy直接代理整个对象,代码更简洁,维护成本更低。
4. Vue3 响应式数据创建:ref vs reactive(实战篇)
Vue3 提供了两种创建响应式数据的 API:ref和reactive。很多开发者会混淆它们的用法,其实核心区别在于处理的数据类型不同。
4.1 reactive:引用类型的 "响应式包装器"
4.1.1 核心定义
reactive用于将引用类型数据 (对象、数组、Map、Set等)转为响应式对象,返回一个Proxy代理实例。
4.1.2 典型使用场景
- 复杂对象(如用户信息、表单数据);
- 数组(如列表数据、选项数组);
- 集合类型(如
Map存储键值对数据)。
4.1.3 代码示例(实战级)
html
<template>
<div class="user-info">
<h3>用户信息</h3>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<button @click="updateUser">修改用户</button>
<h3>爱好列表</h3>
<ul>
<li v-for="(hobby, index) in hobbies" :key="index">{{ hobby }}</li>
</ul>
<button @click="addHobby">新增爱好</button>
</div>
</template>
<script setup>
import { reactive } from 'vue';
// 1. 响应式对象(用户信息)
const user = reactive({
name: '张三',
age: 25
});
// 2. 响应式数组(爱好列表)
const hobbies = reactive(['篮球', '编程']);
// 修改用户信息
const updateUser = () => {
user.name = '李四'; // 直接修改属性,响应式生效
user.age += 1;
};
// 新增爱好
const addHobby = () => {
hobbies.push('阅读'); // 数组方法,响应式生效
hobbies[0] = '足球'; // 直接修改索引,响应式生效
};
</script>
4.1.4 关键注意事项
❌ 不能直接替换
reactive对象(会丢失响应式):
javascriptconst user = reactive({ name: '张三' }); user = { name: '李四' }; // 错误:替换后不再是Proxy代理对象,响应式失效✅ 应修改对象属性而非替换对象:
javascriptuser.name = '李四'; // 正确 Object.assign(user, { name: '李四' }); // 正确
4.2 ref:基本类型的 "响应式载体"
4.2.1 核心定义
ref用于将基本类型数据 (number、string、boolean等)转为响应式数据,返回一个 "响应式对象",该对象包含一个value属性,通过value访问 / 修改原始值。
4.2.2 底层原理
ref的底层是reactive!它会创建一个{ value: 原始值 }的响应式对象(用reactive包装),所以修改value时会触发响应式更新。
4.2.3 典型使用场景
- 基本类型数据(如计数器、开关状态);
- 单个引用类型数据(如单个用户、单个商品,可简化代码);
- 组件间传递的基本类型 props(需用
ref保持响应式)。
4.2.4 代码示例(实战级)
html
<template>
<div class="counter">
<h3>计数器:{{ count }}</h3>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<h3>开关状态:{{ isOpen ? '开启' : '关闭' }}</h3>
<button @click="toggleOpen">切换状态</button>
<h3>单个商品:{{ product.name }}(价格:{{ product.price }}元)</h3>
<button @click="discount">打8折</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 1. 基本类型:数字
const count = ref(0);
// 2. 基本类型:布尔值
const isOpen = ref(true);
// 3. 引用类型:单个对象(用ref也可,简化代码)
const product = ref({
name: 'Vue3实战教程',
price: 99
});
// 计数器+1
const increment = () => {
count.value += 1; // script中必须通过.value修改
};
// 计数器-1
const decrement = () => {
count.value -= 1;
};
// 切换开关
const toggleOpen = () => {
isOpen.value = !isOpen.value;
};
// 商品打折
const discount = () => {
product.value.price *= 0.8; // 引用类型需通过.value访问属性
};
</script>
4.2.5 关键注意事项
- ✅ 模板中无需写
value:Vue3 会自动解包ref对象,模板中直接访问变量名即可(如{``{ count }}等价于{``{ count.value }});- ❌ script 中必须写
value:ref返回的是响应式对象,必须通过value访问 / 修改原始值;- ✅ 引用类型用
ref也可:ref支持包装引用类型,底层会转为reactive,用法上与reactive的区别仅在于需要多一层value。
4.3 ref vs reactive:核心区别与选择指南
| 特性 | ref | reactive |
|---|---|---|
| 支持数据类型 | 基本类型 + 引用类型 | 仅引用类型 |
| 访问方式 | script 中需.value,模板自动解包 |
直接访问属性,无需.value |
| 底层实现 | 基于reactive(包装为{ value: T }) |
基于Proxy直接代理对象 |
| 替换整个对象 | 支持(count.value = 10、product.value = { ... }) |
不支持(直接替换会丢失响应式) |
| 适用场景 | 基本类型、单个引用类型 | 复杂对象、数组、集合类型 |
选择原则(一句话总结):
- 基本类型用 ref (
number、string、boolean等);- 引用类型优先用 reactive (对象、数组、
Map等);- 若需替换整个引用类型对象 (如
product = { name: '新商品' }),用ref更方便。
4.4 实用工具:toRef、toRefs、unref
Vue3 提供了三个常用工具函数,用于解决ref和reactive的转换问题:
4.4.1 toRef:为 reactive 对象的单个属性创建 ref
用于 "提取"reactive对象的单个属性,保持响应式关联:
javascript
import { reactive, toRef } from 'vue';
const user = reactive({ name: '张三', age: 25 });
const nameRef = toRef(user, 'name');
nameRef.value = '李四'; // 修改nameRef,user.name也会变化
console.log(user.name); // 输出:李四
4.4.2 toRefs:将 reactive 对象转为 ref 对象集合
用于 "解构"reactive对象,避免解构后丢失响应式:
javascript
import { reactive, toRefs } from 'vue';
const user = reactive({ name: '张三', age: 25 });
// 直接解构会丢失响应式
const { name, age } = user; // 非响应式
// 用toRefs解构,保持响应式
const { name: nameRef, age: ageRef } = toRefs(user);
nameRef.value = '李四';
console.log(user.name); // 输出:李四(响应式关联)
4.4.3 unref:自动解包 ref 对象
相当于isRef(val) ? val.value : val,简化ref对象的访问:
javascript
import { ref, unref } from 'vue';
const count = ref(0);
console.log(unref(count)); // 输出:0(等价于count.value)
const num = 10;
console.log(unref(num)); // 输出:10(非ref直接返回原值)
5. Vue2 vs Vue3 响应式:全方位对比(表格)
| 对比维度 | Vue2(Object.defineProperty) | Vue3(Proxy + Reflect) |
|---|---|---|
| 核心 API | Object.defineProperty | Proxy + Reflect |
| 劫持粒度 | 单个属性 | 整个对象 |
| 对象新增属性 | 不支持(需 Vue.set) | 原生支持 |
| 对象删除属性 | 不支持(需 Vue.delete) | 原生支持 |
| 数组索引修改 | 不支持 | 原生支持 |
| 数组长度修改 | 不支持 | 原生支持 |
| 嵌套对象处理 | 初始化递归劫持(性能差) | 访问时懒加载代理(性能优) |
| 支持数据类型 | 对象、数组(有限支持) | 对象、数组、Map、Set 等 |
| 原始值处理 | 需包装在对象中 | 用 ref 包装(底层 reactive) |
| 依赖收集核心 | Dep + Watcher | effect + targetMap |
6. 避坑指南:响应式开发常见错误与解决方案
6.1 坑 1:解构 reactive 对象导致响应式丢失
javascript
// 错误:解构reactive对象,属性变为非响应式
const user = reactive({ name: '张三', age: 25 });
const { name, age } = user;
name = '李四'; // 视图不更新
// 解决方案:用toRefs解构
const { name: nameRef, age: ageRef } = toRefs(user);
nameRef.value = '李四'; // 视图更新
6.2 坑 2:直接替换 reactive 对象导致响应式丢失
javascript
// 错误:替换reactive对象,失去Proxy代理
const user = reactive({ name: '张三' });
user = { name: '李四' }; // 不再是响应式对象
// 解决方案1:修改属性而非替换对象
user.name = '李四';
// 解决方案2:用ref包装(需.value替换)
const user = ref({ name: '张三' });
user.value = { name: '李四' }; // 响应式生效
6.3 坑 3:ref 在 script 中忘记写.value
javascript
// 错误:script中直接修改ref变量,不生效
const count = ref(0);
count = 1; // 直接赋值,修改的是变量引用,而非.value
// 正确:通过.value修改
count.value = 1;
6.4 坑 4:认为 ref 只能用于基本类型
javascript
// 误区:ref只能处理基本类型
// 正确:ref支持包装引用类型,底层是reactive
const product = ref({ name: 'Vue3', price: 99 });
product.value.price = 88; // 响应式生效
6.5 坑 5:Proxy 无法监听原始值(需用 ref)
javascript
// 错误:直接用Proxy代理原始值,无效
const count = 0;
const reactiveCount = new Proxy(count, {}); // Proxy不支持原始值
// 正确:用ref包装原始值
const count = ref(0); // 底层转为reactive({ value: 0 })
7. 总结:响应式的本质与最佳实践
7.1 核心本质
Vue 的响应式系统,本质是 **"数据劫持 + 依赖收集"** 的订阅 - 发布模式:
- Vue2 用
Object.defineProperty劫持单个属性,实现简单但限制多;- Vue3 用
Proxy代理整个对象,配合Reflect和effect,解决了 Vue2 的所有痛点,功能更强大、性能更优。
7.2 最佳实践
- 数据类型决定 API 选择 :基本类型用
ref,引用类型优先用reactive; - 避免解构 reactive 对象 :如需解构,用
toRefs保持响应式; - 不替换 reactive 对象 :修改属性而非替换整个对象,或用
ref包装; - script 中牢记 ref 的.value:模板自动解包,但 script 中必须显式访问;
- 复杂场景用工具函数 :
toRef、toRefs、unref简化转换逻辑。
7.3 未来展望
Vue3 的响应式系统基于 ES6 的Proxy,虽然存在 "不支持 IE 浏览器" 的兼容性问题(Vue3 已放弃 IE 支持),但在现代浏览器中,它的优势远大于限制。随着前端技术的发展,Proxy的灵活性和强大功能,将成为响应式系统的主流实现方案。
掌握响应式原理,不仅能让你在开发中避开各种 "坑",更能让你理解 Vue 的设计思想 ------"数据驱动视图" 的核心,正是通过响应式系统,让开发者从繁琐的 DOM 操作中解放出来,专注于业务逻辑。
如果本文对你有帮助,欢迎点赞、收藏、转发!如有疑问或补充,欢迎在评论区交流~
附录:Vue3 响应式 API 速查表
| API | 作用 | 适用场景 |
|---|---|---|
reactive |
将引用类型转为响应式对象 | 复杂对象、数组、集合类型 |
ref |
将基本类型 / 引用类型转为响应式数据 | 基本类型、单个引用类型(需替换) |
toRef |
提取 reactive 对象的单个属性为 ref | 单独使用 reactive 对象的某个属性 |
toRefs |
将 reactive 对象转为 ref 对象集合 | 解构 reactive 对象,保持响应式 |
unref |
自动解包 ref 对象(ref→原始值) | 简化 ref 对象的访问 |
isRef |
判断是否为 ref 对象 | 类型判断 |
isReactive |
判断是否为 reactive 响应式对象 | 类型判断 |
shallowRef |
浅响应式(只监听.value 的替换) | 无需深度响应的引用类型 |
shallowReactive |
浅响应式(只监听顶层属性) | 无需深度响应的复杂对象 |
