Vue 响应式原理
约 1460 字大约 5 分钟
Vue响应式
2026-04-16
响应式系统概述
什么是响应式
响应式是指当数据发生变化时,视图能够自动更新。简单来说,就是数据与视图的自动关联。
// 响应式示例
const data = { name: 'Vue' };
// 当修改 data.name 时,视图自动更新
data.name = 'Vue 3'; // 视图同步更新为什么需要响应式
- 解放手动更新:无需手动操作 DOM,数据变化自动触发视图更新
- 声明式编程:开发者只关心数据,框架负责视图同步
- 开发效率:减少样板代码,提高应用可维护性
Vue 2 响应式原理
Object.defineProperty
Vue 2 通过 Object.defineProperty 实现响应式,将数据对象的属性转换为 getter/setter。
// 简单实现
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`读取 ${key}: ${value}`);
return value;
},
set(newValue) {
if (newValue !== value) {
console.log(`设置 ${key}: ${newValue}`);
value = newValue;
// 触发更新
update();
}
}
});
}依赖收集
每个响应式属性有一个依赖管理器,记录哪些地方依赖了该属性。
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if (target) {
this.subscribers.push(target);
}
}
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 使用
const dep = new Dep();
let price = 5;
let quantity = 2;
let total = 0;
function calc() {
total = price * quantity;
}
// 收集依赖
dep.depend();
calc();
dep.notify(); // total 更新为 10触发更新
当数据变化时,响应式系统通知所有依赖进行更新。
// Watcher 实现
class Watcher {
constructor(fn) {
this.fn = fn;
this.value = this.get();
}
update() {
this.value = this.get();
}
get() {
target = this;
this.fn();
target = null;
}
}数组的响应式处理
Vue 2 无法直接拦截数组索引变化,通过重写数组方法实现响应式。
// 数组方法劫持
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
const original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
value: function(...args) {
console.log(`数组调用 ${method}`);
return original.apply(this, args);
},
enumerable: false,
writable: true,
configurable: true
});
});
// 使用
function observeArray(arr) {
arr.__proto__ = arrayMethods;
}存在的问题
Vue 2 响应式存在以下局限:
- 新增属性:由于
Object.defineProperty只作用于定义时已存在的属性,新增属性不会触发响应式
// 解决方案:Vue.set / this.$set
Vue.set(this.user, 'age', 18);
this.$set(this.user, 'age', 18);- 删除属性:删除属性也不会触发响应式
// 解决方案:Vue.delete / this.$delete
Vue.delete(this.user, 'name');
this.$delete(this.user, 'name');- 数组索引:直接通过索引设置元素不是响应式的
// 不响应
this.items[0] = { id: 1 }; // 不触发更新
// 响应方式 1:splice
this.items.splice(0, 1, { id: 1 });
// 响应方式 2:Vue.set
Vue.set(this.items, 0, { id: 1 });Vue 3 响应式原理
Proxy 和 Reflect
Vue 3 使用 ES6 Proxy 替代 Object.defineProperty,能够拦截对象的所有操作。
// 基础 reactive 实现
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key);
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key);
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key);
return result;
},
has(target, key) {
track(target, key);
return Reflect.has(target, key);
}
});
}reactive / ref / computed
Vue 3 提供三种响应式 API:
// reactive:深度响应式对象
const state = reactive({
count: 0,
user: { name: 'Vue' }
});
// ref:响应式引用,适用于基本类型
const count = ref(0);
console.log(count.value); // 0
count.value++;
console.log(count.value); // 1
// computed:计算属性
const doubled = computed(() => count.value * 2);// ref 内部实现
class RefImpl {
constructor(value) {
this._value = value;
this.dep = new Set();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
this._value = newVal;
triggerRefValue(this);
}
}
// computed 内部实现
class ComputedRefImpl {
constructor(getter) {
this._getter = getter;
this._value = undefined;
this._dirty = true;
this.dep = new Set();
}
get value() {
if (this._dirty) {
this._value = this._getter();
this._dirty = false;
}
return this._value;
}
}effect 和 track / trigger
Vue 3 的响应式核心是依赖收集和触发更新的分离。
// 全局依赖映射
const targetMap = new WeakMap();
// 依赖收集
function track(target, key) {
const effect = activeEffect;
if (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(effect);
}
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects?.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect);
} else {
effect();
}
});
}
// 创建 effect
function effect(fn, options = {}) {
const effect_ = function() {
try {
activeEffect = effect_;
fn();
} finally {
activeEffect = null;
}
};
effect_.options = options;
effect_();
return effect_;
}Vue 2 vs Vue 3 响应式对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 拦截范围 | 属性级别 | 对象级别 |
| 新增属性 | 需使用 Vue.set | 自动响应 |
| 删除属性 | 需使用 Vue.delete | 自动响应 |
| 数组索引 | 需使用 splice/set | 自动响应 |
| 性能 | 初始化时递归遍历 | Lazy 响应 |
| IE 兼容性 | 支持 IE 9+ | 不支持 IE |
// Vue 2 初始化时递归
function walk(obj) {
for (const key in obj) {
defineReactive(obj, key, obj[key]);
if (typeof obj[key] === 'object') {
walk(obj[key]);
}
}
}
// Vue 3 Lazy 响应
// Proxy 本身是懒执行的,只有在访问时才会收集依赖常见问题
响应式丢失(解构)
解构 reactive 对象会丢失响应式:
const state = reactive({ count: 0 });
// 错误:count 是普通的数字,丢失响应式
const { count } = state;
count++; // 不触发更新
// 正确做法 1:保持引用
const state2 = reactive({ count: 0 });
state2.count++;
// 正确做法 2:使用 toRefs 保持响应式
const state3 = reactive({ count: 0 });
const { count } = toRefs(state3);
count.value++; // 响应式浅响应(shallowReactive / shallowRef)
Vue 3 提供浅响应式 API,只对对象第一层进行响应式:
// shallowReactive:只监听第一层
const state = shallowReactive({
foo: 1,
nested: { bar: 2 } // nested 变化不会触发更新
});
// shallowRef:只追踪 .value 变化
const shallow = shallowRef({ count: 0 });
shallow.value.count = 1; // 不触发更新
shallow.value = { count: 1 }; // 触发更新
// triggerRef 强制更新
triggerRef(shallow);异步更新队列 nextTick
Vue 更新 DOM 是异步的,多个数据变化会被合并到一次更新队列。
// 问题:修改数据后立即获取 DOM 是旧值
this.message = 'updated';
console.log(this.$refs.el.textContent); // 仍是旧值
// 解决方案:nextTick
this.message = 'updated';
this.$nextTick(() => {
console.log(this.$refs.el.textContent); // 新值
});
// 或者使用 async/await
async function update() {
this.message = 'updated';
await this.$nextTick();
console.log(this.$refs.el.textContent);
}
// Promise 形式
import { nextTick } from 'vue';
nextTick().then(() => {
console.log(this.$refs.el.textContent);
});// nextTick 原理
const callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
callbacks.forEach(cb => cb());
}
function nextTick(cb) {
return new Promise(resolve => {
callbacks.push(() => {
cb();
resolve();
});
if (!pending) {
pending = true;
Promise.resolve().then(flushCallbacks);
}
});
}