1. 响应式的基本概念

1.1 什么是响应式?

响应式是一种**“数据变化自动触发更新”**的编程范式,最直观的例子是Excel表格:如果单元格A1是=B1+C1,当B1或C1变化时,A1会自动更新。但普通JavaScript变量不具备这种能力——比如:

let B1 = 1;
let C1 = 2;
let A1 = B1 + C1; // A1=3
B1 = 3; // A1还是3,不会自动更新

要让A1自动更新,需要拦截B1/C1的访问(知道谁依赖了它们)和拦截B1/C1的修改(通知依赖者更新)。这就是Vue响应式系统的核心目标。

1.2 为什么JavaScript需要响应式系统?

Vue组件的视图与状态绑定依赖响应式:当组件的datastate变化时,Vue需要自动更新DOM。如果没有响应式系统,我们得手动调用render()函数——这会让代码变得冗余且易出错。

2. Vue 3响应式的核心:Proxy与Reflect

2.1 为什么选Proxy而不是Object.defineProperty?

Vue 2用Object.defineProperty拦截对象属性,但它有三大局限

  1. 无法监听数组变化:需重写push/pop等数组方法才能触发更新;
  2. 无法监听新增/删除属性:需用Vue.set/Vue.delete手动触发;
  3. 只能监听已存在的属性:初始化时未定义的属性无法追踪。

Vue 3用ES6 Proxy解决了这些问题。Proxy是**“对象的代理器”**,能拦截对象的几乎所有操作(如get/set/deleteProperty等),且天然支持数组和新增属性。

2.2 Proxy的工作原理

Proxy通过new Proxy(target, handler)创建,其中:

  • target:被代理的原始对象(如组件的state);
  • handler:**陷阱(Traps)**对象,定义拦截操作的逻辑(如get拦截属性访问,set拦截属性修改)。

关键陷阱:getset

  • get(target, key, receiver):当访问target[key]时触发,用于追踪依赖(记录“谁用到了这个属性”);
  • set(target, key, value, receiver):当修改target[key]时触发,用于触发更新(通知“依赖这个属性的函数重新执行”)。

简单Proxy示例

const user = { name: 'Alice' };
const proxyUser = new Proxy(user, {
  get(target, key) {
    console.log(`访问了${key}${target[key]}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`修改了${key}:从${target[key]}${value}`);
    target[key] = value;
    return true; // 表示操作成功
  }
});

proxyUser.name; // 输出:访问了name:Alice
proxyUser.name = 'Bob'; // 输出:修改了name:从Alice到Bob

2.3 Reflect:保持原生日志的“工具库”

Reflect是ES6的内置对象,提供了操作对象的原生方法(如Reflect.get/Reflect.set)。Vue用它的原因有两个:

  1. 保持this指向正确Reflect.get(target, key, receiver)会让targetgetter方法中的this指向receiver(即Proxy实例),而直接target[key]会指向target本身;
  2. 返回操作结果Reflect.set会返回布尔值,表示修改是否成功(对严格模式很重要)。

对比示例

const user = {
  name: 'Alice',
  get fullName() {
    return this.name; // 这里的this指向谁?
  }
};

// 不用Reflect:this指向user(原始对象)
const proxy1 = new Proxy(user, {
  get(target, key) {
    return target[key]; // fullName的this是user
  }
});
console.log(proxy1.fullName); // Alice(正确)

// 用Reflect:this指向proxy2(Proxy实例)
const proxy2 = new Proxy(user, {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver); // fullName的this是proxy2
  }
});
proxy2.name = 'Bob';
console.log(proxy2.fullName); // Bob(正确,因为this指向proxy2)

3. Vue响应式系统的具体实现

3.1 核心函数:reactive的实现

Vue的reactive函数是Proxy的封装,用于创建响应式对象。其伪代码如下:

function reactive(target) {
  // 仅代理对象/数组(基本类型用ref)
  if (!isObject(target)) return target;

  return new Proxy(target, {
    get(target, key, receiver) {
      // 1. 获取原始值
      const result = Reflect.get(target, key, receiver);
      // 2. 追踪依赖:记录“当前函数用到了target[key]”
      track(target, key);
      // 3. 递归代理:如果result是对象,继续用reactive包裹
      return isObject(result) ? reactive(result) : result;
    },
    set(target, key, value, receiver) {
      // 1. 获取旧值
      const oldValue = Reflect.get(target, key, receiver);
      // 2. 修改值
      const success = Reflect.set(target, key, value, receiver);
      // 3. 触发更新:如果值变化,通知依赖者重新执行
      if (success && oldValue !== value) {
        trigger(target, key);
      }
      return success;
    }
  });
}

3.2 依赖追踪:track函数

往期文章归档
  • Vue 3组合式API中ref与reactive的核心响应式差异及使用最佳实践是什么? - cmdragon's Blog
  • Vue3响应式系统中,对象新增属性、数组改索引、原始值代理的问题如何解决? - cmdragon's Blog
  • Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析 - cmdragon's Blog
  • Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
  • Vue响应式声明的API差异、底层原理与常见陷阱你都搞懂了吗 - cmdragon's Blog
  • 为什么Vue 3需要ref函数?它的响应式原理与正确用法是什么? - cmdragon's Blog
  • Vue 3中reactive函数如何通过Proxy实现响应式?使用时要避开哪些误区? - cmdragon's Blog
  • Vue3响应式系统的底层原理与实践要点你真的懂吗? - cmdragon's Blog
  • Vue 3模板如何通过编译三阶段实现从声明式语法到高效渲染的跨越 - cmdragon's Blog
  • 快速入门Vue模板引用:从收DOM“快递”到调子组件方法,你玩明白了吗? - cmdragon's Blog
  • 快速入门Vue模板里的JS表达式有啥不能碰?计算属性为啥比方法更能打? - cmdragon's Blog
  • 快速入门Vue的v-model表单绑定:语法糖、动态值、修饰符的小技巧你都掌握了吗? - cmdragon's Blog
  • 快速入门Vue3事件处理的挑战题:v-on、修饰符、自定义事件你能通关吗? - cmdragon's Blog
  • 快速入门Vue3的v-指令:数据和DOM的“翻译官”到底有多少本事? - cmdragon's Blog
  • 快速入门Vue3,插值、动态绑定和避坑技巧你都搞懂了吗? - cmdragon's Blog
  • 想让PostgreSQL快到飞起?先找健康密码还是先换引擎? - cmdragon's Blog
  • 想让PostgreSQL查询快到飞起?分区表、物化视图、并行查询这三招灵不灵? - cmdragon's Blog
  • 子查询总拖慢查询?把它变成连接就能解决? - cmdragon's Blog
  • PostgreSQL全表扫描慢到崩溃?建索引+改查询+更统计信息三招能破? - cmdragon's Blog
  • 复杂查询总拖后腿?PostgreSQL多列索引+覆盖索引的神仙技巧你get没? - cmdragon's Blog
  • 只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速? - cmdragon's Blog
  • B-tree索引像字典查词一样工作?那哪些数据库查询它能加速,哪些不能? - cmdragon's Blog
  • 想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷? - cmdragon's Blog
  • PostgreSQL的“时光机”MVCC和锁机制是怎么搞定高并发的? - cmdragon's Blog
  • PostgreSQL性能暴涨的关键?内存IO并发参数居然要这么设置? - cmdragon's Blog
  • 大表查询慢到翻遍整个书架?PostgreSQL分区表教你怎么“分类”才高效
  • PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
  • PostgreSQL里的子查询和CTE居然在性能上“掐架”?到底该站哪边? - cmdragon's Blog
  • PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
  • PostgreSQL新手SQL总翻车?这7个性能陷阱你踩过没? - cmdragon's Blog
  • PostgreSQL索引选B-Tree还是GiST?“瑞士军刀”和“多面手”的差别你居然还不知道? - cmdragon's Blog
  • 想知道数据库怎么给查询“算成本选路线”?EXPLAIN能帮你看明白? - cmdragon's Blog
  • PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
  • PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
  • 转账不翻车、并发不干扰,PostgreSQL的ACID特性到底有啥魔法? - cmdragon's Blog
  • 银行转账不白扣钱、电商下单不超卖,PostgreSQL事务的诀窍是啥? - cmdragon's Blog
  • PostgreSQL里的PL/pgSQL到底是啥?能让SQL从“说目标”变“讲步骤”? - cmdragon's Blog
  • PostgreSQL视图不存数据?那它怎么简化查询还能递归生成序列和控制权限? - cmdragon's Blog
  • PostgreSQL索引这么玩,才能让你的查询真的“飞”起来? - cmdragon's Blog
  • PostgreSQL的表关系和约束,咋帮你搞定用户订单不混乱、学生选课不重复? - cmdragon's Blog
  • PostgreSQL查询的筛子、排序、聚合、分组?你会用它们搞定数据吗? - cmdragon's Blog
  • PostgreSQL数据类型怎么选才高效不踩坑? - cmdragon's Blog
  • 想解锁PostgreSQL查询从基础到进阶的核心知识点?你都get了吗? - cmdragon's Blog
  • PostgreSQL DELETE居然有这些操作?返回数据、连表删你试过没? - cmdragon's Blog
  • PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
  • PostgreSQL插入数据还在逐条敲?批量、冲突处理、返回自增ID的技巧你会吗? - cmdragon's Blog
  • PostgreSQL的“仓库-房间-货架”游戏,你能建出电商数据库和表吗? - cmdragon's Blog
  • PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
  • 能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
  • 给接口加新字段又不搞崩老客户端?FastAPI的多版本API靠哪三招实现? - cmdragon's Blog
  • 流量突增要搞崩FastAPI?熔断测试是怎么防系统雪崩的? - cmdragon's Blog
  • FastAPI秒杀库存总变负数?Redis分布式锁能帮你守住底线吗 - cmdragon's Blog
  • FastAPI的CI流水线怎么自动测端点,还能让Allure报告美到犯规? - cmdragon's Blog
  • 如何用GitHub Actions为FastAPI项目打造自动化测试流水线? - cmdragon's Blog
  • 如何用Git Hook和CI流水线为FastAPI项目保驾护航? - cmdragon's Blog
  • FastAPI如何用契约测试确保API的「菜单」与「菜品」一致?
  • 为什么TDD能让你的FastAPI开发飞起来? - cmdragon's Blog
  • 如何用FastAPI玩转多模块测试与异步任务,让代码不再“闹脾气”? - cmdragon's Blog
  • 如何在FastAPI中玩转“时光倒流”的数据库事务回滚测试?
免费好用的热门在线工具
  • RAID 计算器 - 应用商店 | By cmdragon
  • 在线PS - 应用商店 | By cmdragon
  • Mermaid 在线编辑器 - 应用商店 | By cmdragon
  • 数学求解计算器 - 应用商店 | By cmdragon
  • 智能提词器 - 应用商店 | By cmdragon
  • 魔法简历 - 应用商店 | By cmdragon
  • Image Puzzle Tool - 图片拼图工具 | By cmdragon
  • 字幕下载工具 - 应用商店 | By cmdragon
  • 歌词生成工具 - 应用商店 | By cmdragon
  • 网盘资源聚合搜索 - 应用商店 | By cmdragon
  • ASCII字符画生成器 - 应用商店 | By cmdragon
  • JSON Web Tokens 工具 - 应用商店 | By cmdragon
  • Bcrypt 密码工具 - 应用商店 | By cmdragon
  • GIF 合成器 - 应用商店 | By cmdragon
  • GIF 分解器 - 应用商店 | By cmdragon
  • 文本隐写术 - 应用商店 | By cmdragon
  • CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
  • 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
  • CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
  • 支持我们 - 成为赞助者 | 免费好用的在线工具
  • AI文本生成图像 - 应用商店 | 免费好用的在线工具
  • 临时邮箱 - 应用商店 | 免费好用的在线工具
  • 二维码解析器 - 应用商店 | 免费好用的在线工具
  • 文本转思维导图 - 应用商店 | 免费好用的在线工具
  • 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
  • 文件隐写工具 - 应用商店 | 免费好用的在线工具
  • IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
  • 快传 - 应用商店 | 免费好用的在线工具
  • 随机抽奖工具 - 应用商店 | 免费好用的在线工具
  • 动漫场景查找器 - 应用商店 | 免费好用的在线工具
  • 时间工具箱 - 应用商店 | 免费好用的在线工具
  • 网速测试 - 应用商店 | 免费好用的在线工具
  • AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
  • 背景替换工具 - 应用商店 | 免费好用的在线工具
  • 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
  • Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
  • 图像对比工具 - 应用商店 | 免费好用的在线工具
  • 图片压缩专业版 - 应用商店 | 免费好用的在线工具
  • 密码生成器 - 应用商店 | 免费好用的在线工具
  • SVG优化器 - 应用商店 | 免费好用的在线工具
  • 调色板生成器 - 应用商店 | 免费好用的在线工具
  • 在线节拍器 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
  • 邮箱验证工具 - 应用商店 | 免费好用的在线工具
  • 书法练习字帖 - 应用商店 | 免费好用的在线工具
  • 金融计算器套件 - 应用商店 | 免费好用的在线工具
  • 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
  • Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
  • IP归属地查询 - 应用商店 | 免费好用的在线工具
  • 图片无损放大 - 应用商店 | 免费好用的在线工具
  • 文本比较工具 - 应用商店 | 免费好用的在线工具
  • IP批量查询工具 - 应用商店 | 免费好用的在线工具
  • 域名查询工具 - 应用商店 | 免费好用的在线工具
  • DNS工具箱 - 应用商店 | 免费好用的在线工具
  • 网站图标生成器 - 应用商店 | 免费好用的在线工具
  • XML Sitemap

track的作用是记录“谁依赖了这个属性”。Vue用全局WeakMap存储依赖关系:

  • targetMapWeakMap<target, Map<key, Set<effect>>>,键是被代理的对象,值是“属性→依赖函数”的映射;
  • activeEffect:当前正在运行的Effect函数(依赖响应式数据的函数)。

track的伪代码:

// 全局存储:target -> key -> effects
const targetMap = new WeakMap();
// 当前运行的Effect(全局变量)
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return; // 没有正在运行的Effect,跳过
  // 1. 获取target的依赖Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 2. 获取key的Effect集合
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  // 3. 添加当前Effect到集合
  deps.add(activeEffect);
}

3.3 更新触发:trigger函数

trigger的作用是执行所有依赖该属性的Effect函数,触发更新。其伪代码:

function trigger(target, key) {
  // 1. 获取target的依赖Map
  const depsMap = targetMap.get(target);
  if (!depsMap) return; // 没有依赖,跳过
  // 2. 获取key的Effect集合
  const deps = depsMap.get(key);
  if (!deps) return;
  // 3. 执行所有Effect
  deps.forEach(effect => effect());
}

3.4 Effect:响应式的“副作用”

Effect依赖响应式数据的函数,当数据变化时自动执行。Vue的watchEffectcomputed都是基于Effect实现的。

Effect的伪代码:

function effect(fn) {
  const reactiveEffect = () => {
    try {
      activeEffect = reactiveEffect; // 标记当前Effect
      return fn(); // 执行fn,触发track
    } finally {
      activeEffect = null; // 清除标记
    }
  };
  reactiveEffect(); // 立即执行一次
  return reactiveEffect;
}

例子:用watchEffect实现计数器

import { reactive, watchEffect } from 'vue';

const state = reactive({ count: 0 });

// 创建Effect:依赖state.count
watchEffect(() => {
  console.log(`Count: ${state.count}`);
});

state.count++; // 输出:Count: 1
state.count++; // 输出:Count: 2

4. 应用场景:响应式在Vue组件中的使用

4.1 组件状态管理:reactiveref

  • reactive:用于复杂对象/数组(如userlist),返回Proxy实例;
  • ref:用于基本类型(如countmessage),因为Proxy不能代理基本类型,所以用refvalue属性包裹(Vue模板会自动解包)。

组件示例

<script setup>
import { reactive, ref } from 'vue';

// 复杂对象用reactive
const user = reactive({
  name: 'Alice',
  age: 25
});

// 基本类型用ref
const count = ref(0);

// 点击事件:修改状态
const increment = () => {
  count.value++; // ref需用.value
  user.age++; // reactive直接修改
};
</script>

<template>
  <div>
    <h1>{{ user.name }} ({{ user.age }})</h1>
    <p>Count: {{ count }}</p> <!-- 模板自动解包ref -->
    <button @click="increment">Increment</button>
  </div>
</template>

4.2 保持响应式:toRefstoRef

问题:解构reactive对象会失去响应式——因为解构出来的是普通变量:

const user = reactive({ name: 'Alice', age: 25 });
const { name, age } = user; // name/age是普通变量,不是Proxy
name = 'Bob'; // 不会触发更新

解决:用toRefsreactive对象的所有属性转为ref,或用toRef转为单个ref

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

const user = reactive({ name: 'Alice', age: 25 });
const { name, age } = toRefs(user); // name/age是ref
const ageRef = toRef(user, 'age'); // 单个ref

4.3 数组的响应式处理

Proxy天然支持数组的所有操作(如push/pop/splice、索引修改、length变化),无需像Vue 2那样重写数组方法。

例子

const list = reactive(['apple', 'banana']);

// push触发更新
list.push('orange'); // 视图自动更新

// 索引修改触发更新
list[0] = 'grape'; // 视图自动更新

// length修改触发更新
list.length = 1; // 视图自动更新

5. 响应式调试与常见问题

5.1 调试工具:onRenderTrackedonRenderTriggered

Vue提供了组件生命周期钩子,用于调试渲染时的依赖追踪和触发:

  • onRenderTracked:组件渲染时,追踪到依赖的响应式数据时触发;
  • onRenderTriggered:组件更新时,触发更新的响应式数据变化时触发。

使用示例

<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue';

onRenderTracked((event) => {
  console.log('Tracked:', event); // 打印依赖信息
  debugger; // 断点调试
});

onRenderTriggered((event) => {
  console.log('Triggered:', event); // 打印触发更新的信息
  debugger;
});
</script>

5.2 computedwatch的调试

  • computed:用onTrack(追踪依赖)和onTrigger(触发更新)选项;
  • watch:同样支持onTrackonTrigger选项。

示例

import { ref, computed } from 'vue';

const count = ref(0);
const doubleCount = computed(() => count.value * 2, {
  onTrack(event) {
    console.log('Computed tracked:', event); // 追踪到count.value
  },
  onTrigger(event) {
    console.log('Computed triggered:', event); // count.value变化触发
  }
});

count.value = 1; // 触发onTrigger

6. 课后Quiz

6.1 问题1:为什么Vue 3用Proxy而不是Vue 2的Object.defineProperty?

答案
Proxy解决了Object.defineProperty的三大局限:

  1. 天然支持数组:无需重写push/pop等方法;
  2. 支持新增/删除属性:无需Vue.set/Vue.delete
  3. 更全面的拦截:能拦截deleteProperty/ownKeys等操作。

6.2 问题2:解构reactive对象后如何保持响应式?

答案
toRefsreactive对象的所有属性转为ref,或用toRef转为单个ref。例如:

const { name, age } = toRefs(user); // name/age是ref,保持响应式

6.3 问题3:refvalue属性在模板中为什么不需要手动写?

答案
Vue在模板编译时会自动解包ref——将{{ count }}转为{{ count.value }},所以无需手动写value

7. 常见报错解决方案

7.1 报错1:TypeError: Cannot read property 'value' of undefined

原因

  • ref未初始化:const count = ref()count.valueundefined);
  • 解构reactive对象时未用toRefsconst { count } = usercount是普通变量,无value属性)。

解决

  • 初始化refconst count = ref(0)
  • 解构用toRefsconst { count } = toRefs(user)

7.2 报错2:新增属性不触发更新

原因
Proxy能监听新增属性,但嵌套对象未用reactive包裹

const user = reactive({});
user.address = { city: 'Beijing' }; // address是普通对象,不是响应式
user.address.city = 'Shanghai'; // 不会触发更新

解决
reactive包裹嵌套对象:

user.address = reactive({ city: 'Beijing' });
user.address.city = 'Shanghai'; // 触发更新

7.3 报错3:数组索引修改不触发更新

原因
直接修改索引时,若数组是普通数组(非reactive),则无法触发更新:

const list = ['apple', 'banana']; // 普通数组
list[0] = 'grape'; // 不会触发更新

解决
reactive创建数组:

const list = reactive(['apple', 'banana']);
list[0] = 'grape'; // 触发更新

参考链接

参考链接:vuejs.org/guide/extra…

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]