跳到主要内容

Vue 3 响应式原理深入

问题

Vue 3 响应式系统是如何工作的?ref 和 reactive 有什么区别?依赖收集和派发更新是怎么实现的?

答案

Vue 3 的响应式系统基于 Proxy 实现,通过依赖收集(track)派发更新(trigger) 机制,实现数据变化自动更新视图。

核心概念

reactive 实现原理

reactive 用于创建对象的响应式代理:

// 简化版 reactive 实现
const targetMap = new WeakMap<object, Map<string | symbol, Set<Function>>>();
let activeEffect: Function | null = null;

function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
// 深层响应式
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
// 值变化时派发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
}
});
}

依赖收集(track)

当读取响应式数据时,将当前正在执行的副作用函数收集为依赖:

function track(target: object, key: string | symbol) {
if (!activeEffect) return;

// 获取 target 对应的 depsMap
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}

// 获取 key 对应的 dep(依赖集合)
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}

// 将当前 effect 添加到依赖集合
dep.add(activeEffect);
}
数据结构
targetMap: WeakMap {
target1: Map {
key1: Set { effect1, effect2 },
key2: Set { effect3 }
},
target2: Map { ... }
}

使用 WeakMap 避免内存泄漏,当目标对象被销毁时,对应的依赖也会被自动回收。

派发更新(trigger)

当修改响应式数据时,触发所有收集的依赖:

function trigger(target: object, key: string | symbol) {
const depsMap = targetMap.get(target);
if (!depsMap) return;

const dep = depsMap.get(key);
if (!dep) return;

// 执行所有收集的副作用函数
dep.forEach(effect => {
// 避免无限循环:如果 effect 正在执行,跳过
if (effect !== activeEffect) {
effect();
}
});
}

effect 副作用函数

effect 是响应式系统的核心,它包装需要响应数据变化的函数:

function effect(fn: Function) {
const effectFn = () => {
activeEffect = effectFn;
// 清除旧依赖,重新收集
cleanup(effectFn);
const result = fn();
activeEffect = null;
return result;
};

// 存储依赖此 effect 的 dep 集合
effectFn.deps = [] as Set<Function>[];
effectFn();

return effectFn;
}

function cleanup(effectFn: Function & { deps: Set<Function>[] }) {
// 从所有依赖集合中移除此 effect
effectFn.deps.forEach(dep => dep.delete(effectFn));
effectFn.deps.length = 0;
}

ref 实现原理

ref 用于创建基本类型的响应式数据:

interface Ref<T> {
value: T;
}

function ref<T>(value: T): Ref<T> {
const refObject = {
get value() {
track(refObject, 'value');
return value;
},
set value(newValue: T) {
if (newValue !== value) {
value = newValue;
trigger(refObject, 'value');
}
}
};
return refObject;
}
ref 为什么需要 .value?

JavaScript 无法直接代理基本类型(number、string、boolean),所以 ref 通过对象包装,使用 .value 属性来实现响应式。

ref vs reactive 对比

特性refreactive
适用类型基本类型 + 对象仅对象类型
访问方式.value直接访问
解构保持响应性丢失响应性
模板中使用自动解包直接使用
重新赋值可以不可以
import { ref, reactive } from 'vue';

// ref - 基本类型
const count = ref(0);
count.value++; // ✅

// ref - 对象(内部自动调用 reactive)
const user = ref({ name: 'Tom' });
user.value.name = 'Jerry'; // ✅
user.value = { name: 'Bob' }; // ✅ 可以整体替换

// reactive - 对象
const state = reactive({ count: 0 });
state.count++; // ✅ 直接访问
// state = { count: 1 }; // ❌ 不能重新赋值

// 解构问题
const { count: c } = state; // ❌ 丢失响应性
const { value } = count; // ❌ 丢失响应性(应该用 toRef)

toRef 和 toRefs

解决 reactive 对象解构丢失响应性的问题:

import { reactive, toRef, toRefs } from 'vue';

const state = reactive({
name: 'Vue',
version: 3
});

// toRef:创建单个属性的 ref
const nameRef = toRef(state, 'name');
nameRef.value = 'Vue.js'; // 修改会同步到原对象

// toRefs:转换所有属性为 ref
const { name, version } = toRefs(state);
name.value = 'Vue 3'; // ✅ 保持响应性

完整示例

import { ref, reactive, effect, computed } from 'vue';

// 创建响应式数据
const count = ref(0);
const user = reactive({
name: 'Tom',
age: 20
});

// effect 自动追踪依赖
effect(() => {
console.log(`count: ${count.value}`);
console.log(`user: ${user.name}, ${user.age}`);
});

// 修改数据会自动触发 effect
count.value++; // 输出: count: 1, user: Tom, 20
user.age = 21; // 输出: count: 1, user: Tom, 21

常见面试问题

Q1: ref 和 reactive 的区别是什么?应该如何选择?

答案

维度refreactive
数据类型任意类型仅对象/数组
访问修改.value直接访问
整体替换✅ 支持❌ 不支持
解构需要 toRefs需要 toRefs
TS 类型推导更好一般

选择建议

  • 基本类型:只能用 ref
  • 对象类型:两者都可以,推荐 ref(更一致的心智模型)
  • 组合式函数返回值:推荐 ref(方便解构)
// 推荐:统一使用 ref
const count = ref(0);
const user = ref({ name: 'Tom' });

// 组合式函数
function useCounter() {
const count = ref(0);
const increment = () => count.value++;
return { count, increment }; // 返回 ref,调用者可直接解构
}

Q2: Vue 3 响应式系统如何处理嵌套对象?

答案

Vue 3 采用**懒代理(Lazy Proxy)**策略,只有在访问嵌套属性时才会递归创建代理:

const state = reactive({
user: {
profile: {
name: 'Tom'
}
}
});

// 初始化时只有最外层被代理
// 访问 state.user 时,user 对象被代理
// 访问 state.user.profile 时,profile 对象被代理
state.user.profile.name = 'Jerry'; // 触发更新

相比 Vue 2 的递归遍历,性能更好。

Q3: shallowRef 和 shallowReactive 的作用是什么?

答案

它们只对第一层数据做响应式处理,常用于性能优化

import { shallowRef, shallowReactive, triggerRef } from 'vue';

// shallowRef:只有 .value 的替换是响应式的
const state = shallowRef({ count: 0 });
state.value.count++; // ❌ 不会触发更新
state.value = { count: 1 }; // ✅ 触发更新

// 手动触发更新
state.value.count++;
triggerRef(state);

// shallowReactive:只有第一层属性是响应式的
const shallow = shallowReactive({
nested: { count: 0 }
});
shallow.nested = { count: 1 }; // ✅ 触发更新
shallow.nested.count++; // ❌ 不会触发更新

使用场景:大型不可变数据结构、第三方库对象。

Q4: 为什么 Vue 3 使用 WeakMap 存储依赖?

答案

WeakMap 的键是弱引用,不会阻止垃圾回收:

const targetMap = new WeakMap();

let obj = { name: 'test' };
targetMap.set(obj, new Map());

obj = null; // obj 被回收,targetMap 中对应的依赖也会被自动清除

优势:

  1. 防止内存泄漏:组件销毁后,响应式对象的依赖自动清除
  2. 无需手动清理:不需要显式删除依赖关系

Q5: shallowRef 和 shallowReactive 的作用和使用场景?

答案

shallowRefshallowReactive 是 Vue 3 提供的浅层响应式 API,只对数据的第一层做代理,深层属性的变化不会触发更新。这在处理大型数据结构或不需要深层响应式的场景中,可以显著提升性能。

shallowRef

import { shallowRef, triggerRef, watchEffect } from 'vue';

const largeList = shallowRef<{ id: number; data: Record<string, any> }[]>([
{ id: 1, data: { value: 100, nested: { deep: true } } },
{ id: 2, data: { value: 200, nested: { deep: false } } },
]);

watchEffect(() => {
console.log('列表长度:', largeList.value.length);
});

// ❌ 不会触发更新 —— 只修改了深层属性
largeList.value[0].data.value = 999;

// ✅ 触发更新 —— 替换整个 .value
largeList.value = [...largeList.value];

// ✅ 手动触发更新 —— 配合 triggerRef
largeList.value[0].data.value = 888;
triggerRef(largeList);

shallowReactive

import { shallowReactive, watchEffect } from 'vue';

const state = shallowReactive({
count: 0,
nested: {
msg: 'hello',
deep: { flag: true },
},
});

watchEffect(() => {
console.log('count:', state.count, 'msg:', state.nested.msg);
});

// ✅ 第一层属性变化 → 触发更新
state.count++;

// ✅ 替换第一层引用 → 触发更新
state.nested = { msg: 'world', deep: { flag: false } };

// ❌ 修改深层属性 → 不触发更新
state.nested.msg = 'vue';
state.nested.deep.flag = false;

对比总结

特性ref / reactiveshallowRef / shallowReactive
深层响应式✅ 自动递归代理❌ 只代理第一层
性能开销较高(大数据结构)较低
触发更新方式修改任意层级属性替换第一层引用 / triggerRef
适用场景
  • 第三方库实例:如 echarts 实例、monaco-editor 实例,不需要深层追踪
  • 大型不可变数据:如从 API 获取的大列表,只需要整体替换
  • Canvas / WebGL 对象:这类对象内部状态不需要被 Vue 追踪

Q6: Vue 3 如何追踪数组变化?和 Vue 2 的区别?

答案

Vue 2 的问题

Vue 2 使用 Object.defineProperty,无法检测以下数组操作:

// Vue 2 中无法检测的操作
const arr = [1, 2, 3];

// ❌ 通过索引直接设置值
arr[0] = 10; // 不触发更新

// ❌ 修改 length
arr.length = 1; // 不触发更新

Vue 2 的解决方案是重写数组原型方法pushpopshiftunshiftsplicesortreverse),对这 7 个方法做拦截:

// Vue 2 数组方法拦截(简化版)
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

methodsToPatch.forEach((method) => {
const original = arrayProto[method as keyof typeof arrayProto] as Function;
Object.defineProperty(arrayMethods, method, {
value(...args: unknown[]) {
const result = original.apply(this, args);
// 对新增元素做响应式处理
const ob = (this as any).__ob__;
let inserted: unknown[] | undefined;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
ob.dep.notify(); // 派发更新
return result;
},
});
});

Vue 3 的方案

Vue 3 使用 Proxy 原生拦截所有操作,无需任何 hack

import { reactive, watchEffect } from 'vue';

const arr = reactive([1, 2, 3]);

watchEffect(() => {
console.log('数组内容:', arr.join(', '));
});

// ✅ 索引赋值 → 触发更新
arr[0] = 10;

// ✅ length 修改 → 触发更新
arr.length = 1;

// ✅ 原型方法 → 触发更新
arr.push(4);
arr.splice(0, 1, 99);

Proxy 内部会拦截 getsetdeleteProperty 等操作,对数组的 push 等方法也能正确追踪(因为 push 会触发 setlength 变更)。

对比总结

维度Vue 2Vue 3
底层实现Object.defineProperty + 重写数组方法Proxy 原生拦截
索引赋值❌ 需要 Vue.set()✅ 直接支持
length 修改❌ 不支持✅ 直接支持
新增属性❌ 需要 Vue.set()✅ 直接支持
删除属性❌ 需要 Vue.delete()✅ 直接支持
性能需额外重写方法原生 Proxy,更高效
Vue 2 遗留的 hack

在 Vue 2 中如果需要通过索引修改数组元素,必须使用 Vue.set(arr, index, value)arr.splice(index, 1, newValue)。在 Vue 3 中这些都不再需要。

Q7: effectScope 的作用是什么?在什么场景使用?

答案

effectScope 是 Vue 3.2 引入的 API,用于创建一个副作用作用域,可以批量收集和统一销毁内部的所有响应式副作用(effectcomputedwatchwatchEffect 等)。

基本用法

import { effectScope, ref, computed, watch, watchEffect } from 'vue';

const scope = effectScope();

const counter = ref(0);

scope.run(() => {
// 以下所有副作用都会被 scope 收集
const doubled = computed(() => counter.value * 2);

watchEffect(() => {
console.log(`counter: ${counter.value}, doubled: ${doubled.value}`);
});

watch(counter, (newVal, oldVal) => {
console.log(`counter 变化: ${oldVal}${newVal}`);
});
});

counter.value++; // 触发上述所有副作用

// 一次性停止所有副作用
scope.stop();

counter.value++; // 不再触发任何副作用

嵌套作用域

import { effectScope, ref, watchEffect } from 'vue';

const outerScope = effectScope();
const count = ref(0);

outerScope.run(() => {
watchEffect(() => {
console.log('外层:', count.value);
});

// 嵌套的作用域默认随父作用域销毁
const innerScope = effectScope();
innerScope.run(() => {
watchEffect(() => {
console.log('内层:', count.value);
});
});

// detached 模式:不随父作用域销毁
const detachedScope = effectScope(true);
detachedScope.run(() => {
watchEffect(() => {
console.log('独立:', count.value);
});
});
});

outerScope.stop();
// 外层 → 停止
// 内层 → 停止(随父作用域)
// 独立 → 继续运行(detached 模式)

典型使用场景

场景一:组合式函数(Composable)中管理副作用

import { effectScope, ref, onScopeDispose, watchEffect } from 'vue';

function useMouseTracker() {
const scope = effectScope();
const x = ref(0);
const y = ref(0);

scope.run(() => {
const handler = (e: MouseEvent) => {
x.value = e.clientX;
y.value = e.clientY;
};

window.addEventListener('mousemove', handler);

// onScopeDispose 在作用域销毁时调用
onScopeDispose(() => {
window.removeEventListener('mousemove', handler);
});

watchEffect(() => {
console.log(`鼠标位置: (${x.value}, ${y.value})`);
});
});

const dispose = () => scope.stop();

return { x, y, dispose };
}

场景二:组件外的全局状态管理

import { effectScope, ref, computed } from 'vue';

// 在组件外创建全局响应式状态
let scope: ReturnType<typeof effectScope>;
let state: { count: ReturnType<typeof ref<number>>; doubled: ReturnType<typeof computed<number>> };

function initStore() {
scope = effectScope(true); // detached,不随组件销毁
state = scope.run(() => {
const count = ref(0);
const doubled = computed(() => count.value * 2);
return { count, doubled };
})!;
}

function destroyStore() {
scope.stop();
}
Pinia 中的 effectScope

Pinia 内部就使用了 effectScope 来管理每个 store 的响应式副作用。当 store 被卸载时,调用 scope.stop() 即可一次性清理所有 computedwatch 等副作用,无需逐个手动清除。

Q8: 如何手写一个简单的响应式系统?(effect / track / trigger 核心实现)

答案

下面是一个完整的迷你响应式系统实现,包含 reactiverefeffectcomputed 等核心功能:

核心类型定义

mini-reactivity/types.ts
type Dep = Set<ReactiveEffect>;
type KeyToDepMap = Map<string | symbol, Dep>;

interface ReactiveEffect {
(): void;
deps: Dep[];
options?: EffectOptions;
}

interface EffectOptions {
lazy?: boolean; // 是否延迟执行
scheduler?: (effect: ReactiveEffect) => void; // 自定义调度器
}

effect / track / trigger 实现

mini-reactivity/effect.ts
const targetMap = new WeakMap<object, KeyToDepMap>();
let activeEffect: ReactiveEffect | null = null;
const effectStack: ReactiveEffect[] = []; // 处理嵌套 effect

function effect(fn: () => void, options?: EffectOptions): ReactiveEffect {
const effectFn: ReactiveEffect = () => {
// 清除旧依赖
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
const result = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
return result;
};

effectFn.deps = [];
effectFn.options = options;

// 非 lazy 模式立即执行
if (!options?.lazy) {
effectFn();
}

return effectFn;
}

function cleanup(effectFn: ReactiveEffect) {
effectFn.deps.forEach((dep) => dep.delete(effectFn));
effectFn.deps.length = 0;
}

function track(target: object, key: string | symbol) {
if (!activeEffect) return;

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()));
}

if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}

function trigger(target: object, key: string | symbol) {
const depsMap = targetMap.get(target);
if (!depsMap) return;

const dep = depsMap.get(key);
if (!dep) return;

// 复制一份,避免无限循环
const effectsToRun = new Set<ReactiveEffect>();
dep.forEach((effect) => {
// 防止 effect 在执行中又触发自身
if (effect !== activeEffect) {
effectsToRun.add(effect);
}
});

effectsToRun.forEach((effect) => {
if (effect.options?.scheduler) {
effect.options.scheduler(effect); // 使用调度器
} else {
effect(); // 直接执行
}
});
}
effectStack 的作用

使用栈结构处理嵌套 effect 场景。当外层 effect 执行时内部又创建了 effect,栈确保内层 effect 执行完毕后 activeEffect 能恢复为外层 effect,避免依赖收集错误。

reactive 实现

mini-reactivity/reactive.ts
const reactiveMap = new WeakMap<object, object>();

function reactive<T extends object>(target: T): T {
// 避免重复代理
if (reactiveMap.has(target)) {
return reactiveMap.get(target) as T;
}

const proxy = new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
// 深层对象懒代理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if (!Object.is(oldValue, value)) {
trigger(target, key); // 派发更新
}
return result;
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key);
}
return result;
},
});

reactiveMap.set(target, proxy);
return proxy;
}

ref 实现

mini-reactivity/ref.ts
interface Ref<T> {
value: T;
__isRef: true;
}

function ref<T>(rawValue: T): Ref<T> {
const r: Ref<T> = {
__isRef: true,
get value() {
track(r, 'value');
return rawValue;
},
set value(newValue: T) {
if (!Object.is(rawValue, newValue)) {
rawValue = newValue;
trigger(r, 'value');
}
},
};
return r;
}

function isRef(val: any): val is Ref<any> {
return val?.__isRef === true;
}

computed 实现

mini-reactivity/computed.ts
function computed<T>(getter: () => T): Readonly<Ref<T>> {
let cachedValue: T;
let dirty = true; // 标记是否需要重新计算

const effectFn = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true;
trigger(obj, 'value'); // 通知依赖 computed 的 effect
}
},
});

const obj: Ref<T> = {
__isRef: true,
get value() {
if (dirty) {
cachedValue = effectFn() as T;
dirty = false;
}
track(obj, 'value'); // computed 也可以被其他 effect 依赖
return cachedValue;
},
set value(_: T) {
console.warn('computed 是只读的');
},
};

return obj;
}

运行示例

mini-reactivity/demo.ts
// 1. reactive 基础测试
const state = reactive({ count: 0, msg: 'hello' });

effect(() => {
console.log(`count = ${state.count}`);
});
// 输出: count = 0

state.count++;
// 输出: count = 1

// 2. ref 测试
const name = ref('Vue');

effect(() => {
console.log(`name = ${name.value}`);
});
// 输出: name = Vue

name.value = 'Vue 3';
// 输出: name = Vue 3

// 3. computed 测试
const price = ref(100);
const quantity = ref(3);
const total = computed(() => price.value * quantity.value);

effect(() => {
console.log(`总价: ${total.value}`);
});
// 输出: 总价: 300

price.value = 200;
// 输出: 总价: 600(computed 缓存失效,重新计算)

quantity.value = 5;
// 输出: 总价: 1000
注意事项

上述实现是简化版本,Vue 3 源码中还包含以下优化:

  • 位运算标记:用 TrackOpTypesTriggerOpTypes 区分操作类型
  • 调度队列:通过 queueJob 合并同一 tick 内的多次更新
  • 依赖清理优化:Vue 3.4 引入的双色标记算法,避免每次都重建依赖集合

相关链接