1. 响应式的基本概念
1.1 什么是响应式?
响应式是一种**"数据变化自动触发更新"**的编程范式,最直观的例子是Excel表格:如果单元格A1是=B1+C1,当B1或C1变化时,A1会自动更新。但普通JavaScript变量不具备这种能力------比如:
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组件的视图与状态绑定 依赖响应式:当组件的data或state变化时,Vue需要自动更新DOM。如果没有响应式系统,我们得手动调用render()函数------这会让代码变得冗余且易出错。
2. Vue 3响应式的核心:Proxy与Reflect
2.1 为什么选Proxy而不是Object.defineProperty?
Vue 2用Object.defineProperty拦截对象属性,但它有三大局限:
- 无法监听数组变化 :需重写
push/pop等数组方法才能触发更新; - 无法监听新增/删除属性 :需用
Vue.set/Vue.delete手动触发; - 只能监听已存在的属性:初始化时未定义的属性无法追踪。
Vue 3用ES6 Proxy 解决了这些问题。Proxy是**"对象的代理器"**,能拦截对象的几乎所有操作(如get/set/deleteProperty等),且天然支持数组和新增属性。
2.2 Proxy的工作原理
Proxy通过new Proxy(target, handler)创建,其中:
target:被代理的原始对象(如组件的state);handler:**陷阱(Traps)**对象,定义拦截操作的逻辑(如get拦截属性访问,set拦截属性修改)。
关键陷阱:get与set
get(target, key, receiver):当访问target[key]时触发,用于追踪依赖(记录"谁用到了这个属性");set(target, key, value, receiver):当修改target[key]时触发,用于触发更新(通知"依赖这个属性的函数重新执行")。
简单Proxy示例:
javascript
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用它的原因有两个:
- 保持
this指向正确 :Reflect.get(target, key, receiver)会让target的getter方法中的this指向receiver(即Proxy实例),而直接target[key]会指向target本身; - 返回操作结果 :
Reflect.set会返回布尔值,表示修改是否成功(对严格模式很重要)。
对比示例:
javascript
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的封装,用于创建响应式对象。其伪代码如下:
javascript
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
-
Vue 3中watch侦听器的正确使用姿势你掌握了吗?深度监听、与watchEffect的差异及常见报错解析 - cmdragon's Blog
-
Vue 3中reactive函数如何通过Proxy实现响应式?使用时要避开哪些误区? - cmdragon's Blog
-
快速入门Vue的v-model表单绑定:语法糖、动态值、修饰符的小技巧你都掌握了吗? - cmdragon's Blog
-
只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速? - cmdragon's Blog
-
想抓PostgreSQL里的慢SQL?pg_stat_statements基础黑匣子和pg_stat_monitor时间窗,谁能帮你更准揪出性能小偷? - cmdragon's Blog
-
PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数? - cmdragon's Blog
-
PostgreSQL选Join策略有啥小九九?Nested Loop/Merge/Hash谁是它的菜? - cmdragon's Blog
-
PostgreSQL索引选B-Tree还是GiST?"瑞士军刀"和"多面手"的差别你居然还不知道? - cmdragon's Blog
-
PostgreSQL处理SQL居然像做蛋糕?解析到执行的4步里藏着多少查询优化的小心机? - cmdragon's Blog
-
PostgreSQL备份不是复制文件?物理vs逻辑咋选?误删还能精准恢复到1分钟前? - cmdragon's Blog
-
PostgreSQL里的PL/pgSQL到底是啥?能让SQL从"说目标"变"讲步骤"? - cmdragon's Blog
-
PostgreSQL UPDATE语句怎么玩?从改邮箱到批量更新的避坑技巧你都会吗? - cmdragon's Blog
-
PostgreSQL 17安装总翻车?Windows/macOS/Linux避坑指南帮你搞定? - cmdragon's Blog
-
能当关系型数据库还能玩对象特性,能拆复杂查询还能自动管库存,PostgreSQL凭什么这么香? - cmdragon's Blog
-
如何在FastAPI中玩转"时光倒流"的数据库事务回滚测试?
免费好用的热门在线工具
track的作用是记录"谁依赖了这个属性" 。Vue用全局WeakMap存储依赖关系:
targetMap:WeakMap<target, Map<key, Set<effect>>>,键是被代理的对象,值是"属性→依赖函数"的映射;activeEffect:当前正在运行的Effect函数(依赖响应式数据的函数)。
track的伪代码:
javascript
// 全局存储: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函数,触发更新。其伪代码:
javascript
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的watchEffect和computed都是基于Effect实现的。
Effect的伪代码:
javascript
function effect(fn) {
const reactiveEffect = () => {
try {
activeEffect = reactiveEffect; // 标记当前Effect
return fn(); // 执行fn,触发track
} finally {
activeEffect = null; // 清除标记
}
};
reactiveEffect(); // 立即执行一次
return reactiveEffect;
}
例子:用watchEffect实现计数器
javascript
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 组件状态管理:reactive与ref
reactive:用于复杂对象/数组 (如user、list),返回Proxy实例;ref:用于基本类型 (如count、message),因为Proxy不能代理基本类型,所以用ref的value属性包裹(Vue模板会自动解包)。
组件示例:
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 保持响应式:toRefs与toRef
问题 :解构reactive对象会失去响应式------因为解构出来的是普通变量:
javascript
const user = reactive({ name: 'Alice', age: 25 });
const { name, age } = user; // name/age是普通变量,不是Proxy
name = 'Bob'; // 不会触发更新
解决 :用toRefs将reactive对象的所有属性转为ref,或用toRef转为单个ref:
javascript
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那样重写数组方法。
例子:
javascript
const list = reactive(['apple', 'banana']);
// push触发更新
list.push('orange'); // 视图自动更新
// 索引修改触发更新
list[0] = 'grape'; // 视图自动更新
// length修改触发更新
list.length = 1; // 视图自动更新
5. 响应式调试与常见问题
5.1 调试工具:onRenderTracked与onRenderTriggered
Vue提供了组件生命周期钩子,用于调试渲染时的依赖追踪和触发:
onRenderTracked:组件渲染时,追踪到依赖的响应式数据时触发;onRenderTriggered:组件更新时,触发更新的响应式数据变化时触发。
使用示例:
vue
<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue';
onRenderTracked((event) => {
console.log('Tracked:', event); // 打印依赖信息
debugger; // 断点调试
});
onRenderTriggered((event) => {
console.log('Triggered:', event); // 打印触发更新的信息
debugger;
});
</script>
5.2 computed与watch的调试
computed:用onTrack(追踪依赖)和onTrigger(触发更新)选项;watch:同样支持onTrack和onTrigger选项。
示例:
javascript
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的三大局限:
- 天然支持数组 :无需重写
push/pop等方法; - 支持新增/删除属性 :无需
Vue.set/Vue.delete; - 更全面的拦截 :能拦截
deleteProperty/ownKeys等操作。
6.2 问题2:解构reactive对象后如何保持响应式?
答案 :
用toRefs将reactive对象的所有属性转为ref,或用toRef转为单个ref。例如:
javascript
const { name, age } = toRefs(user); // name/age是ref,保持响应式
6.3 问题3:ref的value属性在模板中为什么不需要手动写?
答案 :
Vue在模板编译时会自动解包ref ------将{{ count }}转为{{ count.value }},所以无需手动写value。
7. 常见报错解决方案
7.1 报错1:TypeError: Cannot read property 'value' of undefined
原因:
ref未初始化:const count = ref()(count.value是undefined);- 解构
reactive对象时未用toRefs:const { count } = user(count是普通变量,无value属性)。
解决:
- 初始化
ref:const count = ref(0); - 解构用
toRefs:const { count } = toRefs(user)。
7.2 报错2:新增属性不触发更新
原因 :
Proxy能监听新增属性,但嵌套对象未用reactive包裹:
javascript
const user = reactive({});
user.address = { city: 'Beijing' }; // address是普通对象,不是响应式
user.address.city = 'Shanghai'; // 不会触发更新
解决 :
用reactive包裹嵌套对象:
javascript
user.address = reactive({ city: 'Beijing' });
user.address.city = 'Shanghai'; // 触发更新
7.3 报错3:数组索引修改不触发更新
原因 :
直接修改索引时,若数组是普通数组 (非reactive),则无法触发更新:
javascript
const list = ['apple', 'banana']; // 普通数组
list[0] = 'grape'; // 不会触发更新
解决 :
用reactive创建数组:
javascript
const list = reactive(['apple', 'banana']);
list[0] = 'grape'; // 触发更新