Vue 响应式:Vue 2 与 Vue 3 响应式原理
Vue 的核心特性之一是响应式系统——当你修改数据时,视图会自动更新。这一能力在 Vue 2 和 Vue 3 中实现方式完全不同。理解它们的原理,能帮你写出更高效、更少 bug 的代码。
Vue 2 的响应式:基于 Object.defineProperty
Vue 2 使用 Object.defineProperty 来劫持对象属性的读取(getter)和设置(setter),从而追踪依赖并在数据变化时通知更新。
创建一个响应式对象的步骤如下:
- 遍历目标对象的所有属性。
- 对每个属性调用
Object.defineProperty,重写其get和set方法。 - 在
get中收集依赖(即哪些组件或计算属性用到了这个值)。 - 在
set中触发更新(通知所有依赖重新计算或渲染)。
例如,当你写:
data() {
return {
message: 'Hello'
}
}
Vue 内部会将其转换为:
Object.defineProperty(data, 'message', {
get() {
// 收集当前活跃的 watcher(比如渲染函数)
dep.depend();
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
// 通知所有依赖更新
dep.notify();
}
});
这种方式存在几个硬伤:
- 无法检测新增或删除属性。比如
this.obj.newProp = 'test'不会触发更新,必须用Vue.set(obj, 'newProp', 'test')。 - 无法监听数组索引赋值(如
arr[0] = 'new')和直接修改数组长度(如arr.length = 0)。Vue 2 对数组方法(如push,splice)做了特殊处理,但原生操作无效。 - 初始化时递归遍历所有嵌套属性,性能开销大,尤其对深层对象。
Vue 3 的响应式:基于 Proxy
Vue 3 放弃了 Object.defineProperty,改用 ES6 的 Proxy API 实现响应式。Proxy 可以拦截整个对象的操作,而不仅限于单个属性。
创建响应式对象只需一行:
const reactiveData = reactive({ message: 'Hello' });
内部使用:
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
// 收集依赖
track(obj, key);
return obj[key];
},
set(obj, key, value) {
// 触发更新
trigger(obj, key);
obj[key] = value;
return true;
}
});
}
相比 Vue 2,Proxy 带来三大优势:
- 天然支持动态属性:
obj.newProp = 'value'能被自动拦截并响应。 - 完整支持数组操作:包括
arr[0] = x、arr.length = 0等原生写法。 - 惰性递归:只有访问到嵌套对象时才将其转为响应式,避免无谓性能损耗。
此外,Vue 3 将响应式系统抽离为独立模块 @vue/reactivity,你甚至可以在非 Vue 项目中使用:
import { reactive, effect } from '@vue/reactivity';
const state = reactive({ count: 0 });
effect(() => {
console.log(state.count); // 自动追踪依赖
});
state.count++; // 输出 1
关键差异对比
以下表格总结了 Vue 2 与 Vue 3 响应式系统的核心区别:
| 特性 | Vue 2 (Object.defineProperty) |
Vue 3 (Proxy) |
|---|---|---|
| 新增属性是否响应 | ❌ 需 Vue.set |
✅ 自动响应 |
| 删除属性是否响应 | ❌ 需 Vue.delete |
✅ 自动响应 |
| 数组索引赋值 | ❌ 不响应 | ✅ 响应 |
| 监听整个对象 | ❌ 只能逐属性监听 | ✅ 完整拦截 |
| 性能(深层对象) | ❌ 初始化全递归 | ✅ 按需代理 |
| 浏览器兼容性 | ✅ 支持 IE9+ | ❌ 仅现代浏览器(不支持 IE) |
如何选择?
如果你的项目需要支持 IE 浏览器,只能用 Vue 2,并严格遵守其响应式限制(如避免直接添加属性)。
如果目标环境是现代浏览器,Vue 3 是更优解:
- 编写更自然的 JavaScript,无需额外 API 处理边界情况。
- 获得更好的性能和内存表现,尤其在大型应用中。
- 享受 Composition API 带来的逻辑复用便利,其底层正是基于新的响应式系统。
要迁移旧代码,注意检查是否有以下写法:
- `this.$set(this.obj, 'prop', val)` → 可直接写 `this.obj.prop = val` - `this.arr[index] = newVal` → 在 Vue 3 中已生效,无需 `splice` --- ## 手动触发响应式的正确姿势 即使在 Vue 3 中,也有少数场景需要手动干预: 1. **替换整个对象**: ```js // 错误:失去响应式引用 state.items = newArray; // 正确:保持响应式 state.items = reactive(newArray); ``` 2. **解构响应式对象**: ```js const { count } = state; // count 不再是响应式的 // 应使用 toRefs 保持响应性 import { toRefs } from 'vue'; const { count } = toRefs(state); ``` 3. **异步修改后强制更新**(极罕见): Vue 3 自动批量更新,通常无需手动触发。若遇极端情况,可用 `nextTick` 确保 DOM 更新完成。 --- ## 验证你的代码是否响应 **测试属性是否响应的最简单方法**:在模板或 `computed` 中使用该数据,然后通过控制台修改它。如果视图更新,说明响应式生效。 例如: ```js // 在组件实例上 app.config.globalProperties.$debug = this.someData;
// 控制台执行
$debug.someProp = 'new value';
若页面未刷新,检查:
- 是否在 `data` 中正确定义(Vue 2)
- 是否使用 `reactive` 或 `ref` 包裹(Vue 3)
- 是否意外替换了整个响应式对象
---
```js
// Vue 3 响应式工具速查
import { ref, reactive, isRef, toRef, toRefs, shallowReactive } from 'vue';
// 基础类型用 ref
const count = ref(0); // .value 访问
// 对象用 reactive
const state = reactive({ list: [] });
// 从响应式对象提取单个属性并保持响应
const listRef = toRef(state, 'list');
// 解构多个属性
const { list, user } = toRefs(state);
// 浅层响应(只代理第一层)
const shallow = shallowReactive({ deep: { nested: true } });
// 修改 shallow.deep.nested 不会触发更新
暂无评论,快来抢沙发吧!