Vue3响应式原理那些事
文章目录
- 1 响应式基础:Proxy 与 Reflect
- 1.1 Proxy 代理拦截
- 1.2 Reflect 确保 `this` 指向正确
- 1.2.1 修正 `this` 指向问题
- 1.2.2 统一的操作返回值
- 1.3 与 Vue2 的对比
- 2 依赖收集与触发机制
- 2.1 全局依赖存储结构:WeakMap → Map → Set
- 2.2 依赖收集触发时机
- 2.3 依赖收集核心实现:track 函数
- 2.4 依赖触发:trigger 函数
- 2.5 副作用管理:ReactiveEffect 类
- 2.6 特殊场景处理:ref 的依赖收集
- 2.7 设计亮点
- 3 响应式 API 的封装
- 3.1. `reactive`
- 3.2 `ref`
- 3.3 `reactive` 与 `ref` 核心区别
- 3.4 注意事项
- 4 双向绑定实现(v-model)
- 4.1 原生 DOM 元素的实现原理
- 4.1.1 属性绑定
- 4.1.2 响应式更新
- 4.2 自定义组件的实现原理
- 4.2.1 传统模式(Vue3.4 前)
- 4.2.2 `defineModel` 宏(Vue3.4+)
- 4.3 响应式系统的支撑
- 4.4 性能优化与扩展性
- 5 对比 Vue2 的改进
近期文章:
- Vue3开发常见性能问题知多少
- Vue3组件常见通信方式你了解多少?
- 实现篇:LRU算法的几种实现
- 从底层视角看requestAnimationFrame的性能增强
- Nginx Upstream了解一下
- 实现篇:一文搞懂Promise是如何实现的
- 实现篇:如何手动实现JSON.parse
- 实现篇:如何亲手定制实现JSON.stringify
- 一文搞懂 Markdown 文档规则
Vue3 的双向响应式原理是其核心机制,通过 Proxy 代理与 依赖收集系统 实现数据与视图的自动同步。以下是其核心原理与技术细节的深度解析:
1 响应式基础:Proxy 与 Reflect
Vue3 彻底抛弃了 Vue2 的 Object.defineProperty
,改用 ES6 Proxy 实现数据劫持,解决了 Vue2 无法监听对象属性新增/删除、数组索引修改等问题。
1.1 Proxy 代理拦截
Proxy 可拦截对象的所有操作(如 get
、set
、deleteProperty
等),解决了 Vue2 中 Object.defineProperty
无法监听新增属性和数组索引修改的问题。
Proxy 默认采用惰性代理,仅在访问嵌套对象时递归创建代理,优化了性能。
const handler = {get(target, key, receiver) {track(target, key); // 依赖收集return Reflect.get(target, key, receiver);},set(target, key, value, receiver) {const result = Reflect.set(target, key, value, receiver);trigger(target, key); // 触发更新return result;}
};
const proxyData = new Proxy(data, handler);
1.2 Reflect 确保 this
指向正确
Reflect 的静态方法与 Proxy 的拦截器一一对应,确保操作的一致性和安全性:
1.2.1 修正 this
指向问题
当代理对象包含访问器属性(如 get c() { return this.a + this.b }
)时,若直接通过 target[key]
读取属性,this
会指向原对象而非代理对象,导致后续属性访问无法触发 Proxy 拦截。
解决方案:使用 Reflect.get(target, key, receiver)
,其中 receiver
显式传递代理对象,确保 this
指向正确。
// 错误示例(this指向原对象)get(target, key) { return target[key]; }// 正确示例(通过Reflect修正this)get(target, key, receiver) { return Reflect.get(target, key, receiver); }
1.2.2 统一的操作返回值
Reflect 方法返回布尔值(如 Reflect.set()
返回操作是否成功),简化了错误处理流程。
1.3 与 Vue2 的对比
特性 | Vue2 (Object.defineProperty) | Vue3 (Proxy + Reflect) |
---|---|---|
属性监听 | 需遍历属性逐个劫持 | 代理整个对象,自动处理新增/删除属性 |
数组支持 | 需重写数组方法 | 直接拦截原生数组操作 |
性能 | 初始化递归遍历,性能较差 | 惰性代理,按需触发拦截 |
代码复杂度 | 需手动处理嵌套对象和数组 | 原生支持复杂数据结构 |
2 依赖收集与触发机制
Vue3 的依赖收集机制通过 Proxy 拦截访问 → 全局拓扑存储 → 精准触发更新 的链路,实现了高效、精准的响应式更新。其 WeakMap 结构、ReactiveEffect 双向链接和 惰性代理 等特性,使得框架具备更高的性能。
2.1 全局依赖存储结构:WeakMap → Map → Set
Vue3 使用三级数据结构管理依赖关系,确保高效的内存管理和精准的依赖追踪:
// 源码位置:packages/reactivity/src/effect.ts
type Dep = Set<ReactiveEffect>;
type KeyToDepMap = Map<any, Dep>;
const targetMap = new WeakMap<any, KeyToDepMap>(); // 全局依赖存储// 结构示意:
WeakMap {[targetObject]: Map {[key]: Set<effect1, effect2...>}
}
- WeakMap:键为原始对象(避免内存泄漏),值为
KeyToDepMap
。 - KeyToDepMap:键为对象属性名,值为
Dep
(存储关联的副作用函数集合)。 - Dep:
Set<ReactiveEffect>
,保证副作用的唯一性。
2.2 依赖收集触发时机
当访问响应式对象的属性时,Proxy 的 get
拦截器触发依赖收集流程:
// 源码简化:packages/reactivity/src/baseHandlers.ts
function createGetter() {return function get(target: object, key: string | symbol, receiver: object) {const res = Reflect.get(target, key, receiver);if (shouldTrack) {track(target, TrackOpTypes.GET, key); // 核心收集逻辑}return res;};
}
shouldTrack
:全局开关,仅在副作用执行期间开启收集。activeEffect
:当前活跃的副作用函数(如组件渲染函数)。
2.3 依赖收集核心实现:track 函数
// 源码位置:packages/reactivity/src/effect.ts
export function track(target: object, type: TrackOpTypes, key: unknown) {if (!shouldTrack || !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()));}trackEffects(dep); // 建立双向依赖关联
}function trackEffects(dep: Dep) {dep.add(activeEffect!);activeEffect!.deps.push(dep); // 双向链接:effect ↔ dep
}
- 双向链接:每个
ReactiveEffect
记录其关联的所有Dep
,便于后续清理。 - 版本号机制:
Dep
通过version
标记变更,避免无效依赖执行。
2.4 依赖触发:trigger 函数
当响应式数据修改时,Proxy
的 set
拦截器触发更新:
// 源码简化:packages/reactivity/src/effect.ts
export function trigger(target: object, type: TriggerOpTypes, key?: unknown) {const depsMap = targetMap.get(target);if (!depsMap) return;const effects = new Set<ReactiveEffect>();if (key !== void 0) {const dep = depsMap.get(key);dep?.forEach(effect => effects.add(effect));}// 调度执行(支持异步批量更新)effects.forEach(effect => {if (effect.scheduler) {effect.scheduler();} else {effect.run();}});
}
- 精准触发:仅执行与修改属性关联的副作用。
- 调度器(scheduler):支持异步队列优化(如
watchEffect
的flush: 'post'
)。
2.5 副作用管理:ReactiveEffect 类
副作用(如组件渲染、计算属性)通过 ReactiveEffect
实例化:
// 源码位置:packages/reactivity/src/effect.ts
export class ReactiveEffect<T = any> {deps: Dep[] = []; // 关联的所有依赖集合active = true;constructor(public fn: () => T,public scheduler?: EffectScheduler) {}run() {if (!this.active) return;activeEffect = this; // 标记当前活跃的副作用const res = this.fn();activeEffect = undefined; // 执行完毕后重置return res;}stop() {if (this.active) {cleanupEffect(this); // 清理所有依赖关联this.active = false;}}
}
activeEffect
全局变量:在副作用执行期间标记当前上下文。- 自动清理:当组件卸载时,调用
stop()
断开所有依赖关联。
2.6 特殊场景处理:ref 的依赖收集
对于 ref
类型,依赖收集通过 RefImpl
类的 get value()
触发:
// 源码位置:packages/reactivity/src/ref.ts
class RefImpl<T> {dep?: Dep;get value() {trackRefValue(this); // 调用 track 函数return this._value;}
}export function trackRefValue(ref: RefBase<any>) {if (activeEffect) {trackEffect(ref.dep || (ref.dep = createDep()));}
}
trackRefValue
:将activeEffect
关联到ref.dep
集合中。
2.7 设计亮点
- 惰性代理:仅在属性首次被访问时创建嵌套代理,减少初始化开销。
- WeakMap 内存管理:避免因未使用的对象导致内存泄漏。
- 双向依赖链接:
effect.deps
和dep
双向关联,支持高效清理。 - 按需触发:通过
scheduler
实现异步批量更新,优化渲染性能。
3 响应式 API 的封装
Vue3 提供 reactive
和 ref
两类 API 适应不同场景:
3.1. reactive
将对象转为深度响应式代理,支持嵌套属性、数组方法拦截(如 push
、pop
)。
以下场景有限使用 reactive
:
- 管理复杂状态(如表单数据、嵌套对象)。
- 需要自动深度响应嵌套属性(如树形结构数据)。
- 与
watch
结合监听整个对象变化时。
import { reactive } from 'vue';const state = reactive({name: 'Bob',scores: [80, 90],address: { city: 'New York' }
});
state.scores.push(95); // 修改数组
state.address.city = 'London'; // 修改嵌套属性
3.2 ref
用于基本类型(如 number
、string
),通过 .value
访问值,内部通过对象包装实现响应式。
以下场景有限使用 ref
:
- 管理基本类型(如计数器、开关状态)。
- 需要直接替换整个对象时(如 API 返回数据更新)。
- 模板中需要解构响应式对象。
import { ref } from 'vue';// 基本类型
const count = ref(0);
console.log(count.value); // 0
count.value++; // 修改值// 对象类型
const user = ref({ name: 'Alice', age: 25 });
user.value.age = 26; // 修改属性
user.value = { name: 'Bob' }; // 直接替换整个对象
3.3 reactive
与 ref
核心区别
特性 | ref | reactive |
---|---|---|
适用数据类型 | 所有类型(基本类型、对象、数组) | 仅对象或数组(引用类型) |
访问方式 | 通过 .value 访问和修改 | 直接访问属性(无需 .value ) |
响应式原理 | 包装成 { value: ... } 对象 | 基于 Proxy 代理整个对象 |
解构后响应性 | 保持响应性(需结合 toRef /toRefs ) | 直接解构会丢失响应性 |
对象替换灵活性 | 可直接替换整个对象 | 需修改内部属性,不可直接替换整个对象 |
性能优化 | 适合简单数据(如 number 、string ) | 适合复杂对象(嵌套属性自动深度响应) |
3.4 注意事项
- 避免直接替换
reactive
对象:
const state = reactive({ a: 1 });
// ❌ 错误!破坏响应性
state = { a: 2 };
// ✅ 正确!修改内部属性
state.a = 2;
- 正确使用
.value
:
const count = ref(0);
// 模板中自动解包,无需 .value
<p>{{ count }}</p>
// JS 中必须通过 .value 访问
count.value++;
- 结合
toRefs
解构:
const state = reactive({ a: 1, b: 2 });
const { a, b } = toRefs(state); // 保持响应性
4 双向绑定实现(v-model)
Vue3 的 v-model
基于响应式系统实现视图与数据的双向同步:
- 数据 → 视图:
响应式数据变化时,触发setter
→ 依赖更新 → 重新渲染组件 → 更新 DOM。 - 视图 → 数据:
为表单元素(如<input>
)绑定input
事件,用户输入时更新响应式数据:
// 伪代码:v-model 实现
input.addEventListener('input', (e) => {state.value = e.target.value; // 触发 setter
});
4.1 原生 DOM 元素的实现原理
对于原生输入元素(如 <input>
、<textarea>
),v-model
的底层实现是 属性绑定 和 事件监听 的组合:
4.1.1 属性绑定
将输入元素的 value
属性与 Vue 实例的响应式数据绑定。例如:
<input v-model="msg">
会被编译为:
<input :value="msg" @input="msg = $event.target.value">
:value="msg"
:通过响应式系统将msg
的值同步到输入框的value
属性。@input="msg = $event.target.value"
:监听输入事件,更新msg
的值。
4.1.2 响应式更新
- 当
msg
变化时,触发 Proxy 的set
拦截器,通知依赖更新视图。 - 当用户输入时,通过
input
事件修改msg
,触发响应式系统的更新链。
4.2 自定义组件的实现原理
在自定义组件中,v-model
的实现分为两个阶段:Vue3.4 前的 Props/Events 模式 和 Vue3.4 后的 defineModel
宏。
4.2.1 传统模式(Vue3.4 前)
通过 modelValue
prop 和 update:modelValue
事件实现双向绑定:
<!-- 父组件 -->
<Child v-model="msg" />
<!-- 编译为 -->
<Child :modelValue="msg" @update:modelValue="msg = $event" /><!-- 子组件(Child.vue) -->
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
</script>
<template><input :value="props.modelValue"@input="emit('update:modelValue', $event.target.value)"/>
</template>
modelValue
:父组件传递的响应式数据。update:modelValue
:子组件通过事件通知父组件更新数据。
4.2.2 defineModel
宏(Vue3.4+)
Vue3.4 引入的 defineModel
宏简化了代码:
<!-- 子组件(Child.vue) -->
<script setup>
const model = defineModel(); // 声明双向绑定的变量
</script>
<template><input v-model="model" />
</template>
- 底层展开:
defineModel
会被编译为modelValue
prop 和update:modelValue
事件。 - 支持参数化:例如
v-model:title
会生成title
prop 和update:title
事件。
// 参数化示例
const title = defineModel('title', { required: true }); // 支持参数与校验
4.3 响应式系统的支撑
v-model
的底层依赖 Vue3 的 Proxy 响应式系统:
- 数据劫持:通过
Proxy
拦截对象操作(如get
、set
),结合Reflect
确保this
指向正确。 - 依赖追踪:访问数据时触发
track()
收集依赖(副作用函数),修改数据时触发trigger()
通知更新。
4.4 性能优化与扩展性
- 惰性代理:嵌套对象的 Proxy 代理按需创建,避免初始化性能损耗。
- 多
v-model
绑定:支持为同一组件绑定多个v-model
,例如:
<UserForm v-model:name="userName" v-model:age="userAge"
/>
子组件通过 defineModel('name')
和 defineModel('age')
分别处理。
- 自定义修饰符:通过
v-model.modifier
扩展功能(如v-model.trim
),子组件可解析修饰符逻辑。
场景 | 实现方式 | 核心原理 |
---|---|---|
原生元素 | :value + @input 事件组合 | 属性与事件的双向绑定 |
自定义组件 | modelValue prop + update:modelValue 事件(或 defineModel 宏) | Props/Events 通信 |
响应式支撑 | Proxy 拦截数据操作 + 依赖收集/触发 | 数据驱动视图 |
5 对比 Vue2 的改进
特性 | Vue2 (Object.defineProperty) | Vue3 (Proxy) |
---|---|---|
对象属性监听 | 需遍历属性逐个劫持 | 自动代理整个对象,支持新增/删除属性 |
数组监听 | 需重写数组方法 | 直接拦截 push 、pop 等原生方法 |
性能 | 初始化时递归遍历对象,性能较差 | 惰性代理,按需触发拦截 |
代码复杂度 | 需手动处理数组和嵌套对象 | 原生支持复杂数据结构 |
-
更细粒度的依赖追踪:仅更新实际变化的组件,避免无效渲染。
-
深层响应式支持:嵌套对象/数组的修改自动触发更新。
-
TypeScript 友好:基于 Proxy 的 API 设计更符合类型推导需求。
-
调用链
reactive() → 创建Proxy├── get → track() → 收集effect到依赖树└── set → trigger() → 触发effect更新
通过以上机制,Vue3 实现了高效、灵活的双向响应式系统,为现代前端开发提供了更强大的数据驱动能力。
引用:
- reactivity-fundamentals
- reactivity-in-depth