当前位置: 首页 > ds >正文

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 可拦截对象的所有操作(如 getsetdeleteProperty 等),解决了 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(存储关联的副作用函数集合)。
  • DepSet<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 函数

当响应式数据修改时,Proxyset 拦截器触发更新:

// 源码简化: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):支持异步队列优化(如 watchEffectflush: '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 设计亮点

  1. 惰性代理:仅在属性首次被访问时创建嵌套代理,减少初始化开销。
  2. WeakMap 内存管理:避免因未使用的对象导致内存泄漏。
  3. 双向依赖链接effect.depsdep 双向关联,支持高效清理。
  4. 按需触发:通过 scheduler 实现异步批量更新,优化渲染性能。

3 响应式 API 的封装

Vue3 提供 reactiveref 两类 API 适应不同场景:

3.1. reactive

将对象转为深度响应式代理,支持嵌套属性、数组方法拦截(如 pushpop)。

以下场景有限使用 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

用于基本类型(如 numberstring),通过 .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 reactiveref 核心区别

特性refreactive
适用数据类型所有类型(基本类型、对象、数组)仅对象或数组(引用类型)
访问方式通过 .value 访问和修改直接访问属性(无需 .value
响应式原理包装成 { value: ... } 对象基于 Proxy 代理整个对象
解构后响应性保持响应性(需结合 toRef/toRefs直接解构会丢失响应性
对象替换灵活性可直接替换整个对象需修改内部属性,不可直接替换整个对象
性能优化适合简单数据(如 numberstring适合复杂对象(嵌套属性自动深度响应)

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 拦截对象操作(如 getset),结合 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)
对象属性监听需遍历属性逐个劫持自动代理整个对象,支持新增/删除属性
数组监听需重写数组方法直接拦截 pushpop 等原生方法
性能初始化时递归遍历对象,性能较差惰性代理,按需触发拦截
代码复杂度需手动处理数组和嵌套对象原生支持复杂数据结构
  • 更细粒度的依赖追踪:仅更新实际变化的组件,避免无效渲染。

  • 深层响应式支持:嵌套对象/数组的修改自动触发更新。

  • TypeScript 友好:基于 Proxy 的 API 设计更符合类型推导需求。

  • 调用链

reactive() → 创建Proxy├── get → track() → 收集effect到依赖树└── set → trigger() → 触发effect更新

通过以上机制,Vue3 实现了高效、灵活的双向响应式系统,为现代前端开发提供了更强大的数据驱动能力。

引用:

  • reactivity-fundamentals
  • reactivity-in-depth
http://www.xdnf.cn/news/4149.html

相关文章:

  • PyTorch 张量与自动微分操作
  • 研0大模型学习(第12天)
  • 《深入理解 Java 虚拟机》笔记
  • 三、【LLaMA-Factory实战】模型微调进阶:从LoRA到MoE的技术突破与工程实践
  • 一文读懂Python之pandas模块
  • Vite简单介绍
  • 亚马逊卖家复刻案例:用社群分层策略实现海外用户月均消费3.2次
  • 普通消元求解线性基并求解最大异或和
  • 【论文笔记】SOTR: Segmenting Objects with Transformers
  • 机器人强化学习入门学习笔记
  • 有效的数独(中等)
  • Qt中数据结构使用自定义类————附带详细示例
  • 2025年企业Radius认证服务器市场深度调研:中小企业身份安全投入产出比最优解
  • Untiy基础学习(六)MonoBehaviour基类的简单介绍
  • 形式化数学——Lean求值表达式
  • 【数据治理】数据架构设计
  • 2962. 统计最大元素出现至少 K 次的子数组
  • 1. 设计哲学:让字面量“活”起来,提升表达力和安全性
  • java stream
  • Python训练打卡Day16
  • 【AI绘画】Ottohans Beier风格雕刻版画
  • 我的世界Minecraft游戏服务器搭建教程:腾讯云Java版
  • java CompletableFuture 异步编程工具用法1
  • 免费在线练字宝藏Z2H 免安装高效生成 vs 笔顺功能补缺
  • Docker 容器 - Dockerfile
  • 大模型微调Fine-tuning:从概念到实践的全面解析
  • #基础Machine Learning 算法(上)
  • 第三章 - 软件质量工程体系
  • 【codeforces 2070c】二分答案详解
  • PostgreSQL 的 pg_current_wal_lsn 函数