一、响应式系统的工作原理
Vue3的响应式系统基于Proxy 实现,通过代理对象(或数组)跟踪属性的读取 (get)和修改 (set)操作。当属性变化时,Vue会自动触发依赖更新,同步视图。
但Proxy并非"全能"------它只能跟踪初始存在的属性 和已知的变更操作,对于一些"非常规"修改(如新增属性、直接修改数组索引),无法自动触发响应,需要我们手动规避。
二、对象的局限性:新增/删除属性不响应
1. 问题描述
用reactive创建的响应式对象,新增属性 或删除属性 时,无法触发视图更新。因为Proxy默认只跟踪对象初始化时的已有属性,新增/删除的属性不在初始跟踪范围内。
示例(错误用法):
javascript
import { reactive } from 'vue';
const user = reactive({ name: 'Alice' });
// 新增属性:视图不更新
user.age = 20;
// 删除属性:视图不更新
delete user.name;
2. 规避方案
针对对象的新增/删除操作,Vue3提供了3种标准解决方案:
(1) 使用set/delete函数
set(Vue.set的简写)用于向响应式对象新增属性 ,delete(Vue.delete的简写)用于删除属性,两者都会触发响应式更新。
示例(正确用法):
javascript
import { reactive, set, delete as vueDelete } from 'vue';
const user = reactive({ name: 'Alice' });
// 新增响应式属性
set(user, 'age', 20); // 视图更新为 { name: 'Alice', age: 20 }
// 删除响应式属性
vueDelete(user, 'name'); // 视图更新为 { age: 20 }
(2) 扩展运算符生成新对象
通过扩展运算符 (...)创建新对象,替换原对象。新对象会继承原对象的响应式能力,触发视图更新。
示例:
javascript
const userRef = ref({ name: 'Alice' }); // 用ref包裹对象
// 新增属性:生成新对象
userRef.value = { ...userRef.value, age: 20 }; // 响应式更新
(3) 初始定义所有可能的属性
如果提前知道对象的所有属性,可以在reactive初始化时就定义(值为undefined),后续修改时会自动触发响应。
往期文章归档
-
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中优雅地模拟多模块集成测试? - cmdragon's Blog
免费好用的热门在线工具
示例:
javascript
const user = reactive({ name: 'Alice', age: undefined }); // 初始定义age
user.age = 20; // 响应式更新(视图显示20)
三、数组的局限性:直接修改索引/长度不响应
1. 问题描述
用reactive或ref包裹的数组,直接修改索引 或修改长度 时,Vue3早期版本无法触发响应(官网文档仍保留此说明)。尽管最新版本(v3.4+)已支持,但为了兼容性,仍建议用变异方法 或set函数。
示例(不推荐用法):
javascript
const list = ref([1, 2, 3]);
// 直接修改索引:早期版本不响应
list.value[0] = 4;
// 修改长度:早期版本不响应
list.value.length = 2;
2. 规避方案
Vue3推荐用数组变异方法 或set函数修改数组,确保响应式:
(1) 使用数组变异方法
Vue3自动跟踪数组的变异方法(修改原数组的方法),这些方法会触发响应式更新。常用变异方法:
push():末尾添加元素pop():末尾删除元素splice():插入/删除/替换元素sort():排序reverse():反转
示例:
javascript
const list = ref([1, 2, 3]);
// 修改索引0的值:用splice
list.value.splice(0, 1, 4); // 替换第0个元素为4(响应式)
// 修改长度:用splice
list.value.splice(2); // 删除索引2及之后的元素(长度变为2,响应式)
(2) 使用set函数
set函数可直接修改数组的指定索引,触发响应式更新。
示例:
javascript
import { ref, set } from 'vue';
const list = ref([1, 2, 3]);
// 修改索引0的值:响应式
set(list.value, 0, 4);
四、原始值的局限性:reactive无法代理
1. 问题描述
reactive只能代理对象或数组 ,无法代理原始值 (如number、string、boolean)。直接用reactive包裹原始值会报错。
示例(错误用法):
javascript
import { reactive } from 'vue';
// 报错:value cannot be made reactive: 0
const count = reactive(0);
2. 规避方案:使用ref
ref是Vue3专门用于代理原始值 的API,它会将原始值包裹在一个带value属性的响应式对象中。访问或修改时需通过.value(模板中无需.value,Vue会自动解包)。
示例(正确用法):
javascript
import { ref } from 'vue';
// 用ref代理原始值
const count = ref(0);
// 修改值:响应式
count.value++; // 变为1
// 模板中使用:自动解包
// <div>{{ count }}</div> // 显示1
五、实践案例:购物车功能的响应式处理
我们用上述知识实现一个购物车功能,包含商品的添加、删除和数量修改:
1. 需求分析
- 商品列表:响应式数组,支持添加、删除。
- 数量修改:点击"+"/"-"按钮,更新商品数量并同步总价。
- 总价计算:根据商品数量和单价,实时更新。
2. 完整代码
vue
<template>
<div class="cart">
<h2>购物车</h2>
<!-- 商品列表 -->
<div v-for="(item, index) in cart" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<button @click="decrement(index)">-</button>
<span>{{ item.quantity }}</span>
<button @click="increment(index)">+</button>
<button @click="removeItem(index)">删除</button>
</div>
<!-- 总价 -->
<p class="total">总价:{{ totalPrice }} 元</p>
<!-- 添加商品按钮 -->
<button @click="addItem" class="add-btn">添加商品</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 1. 用ref包裹购物车数组(支持响应式)
const cart = ref([
{ id: 1, name: '商品A', quantity: 1, price: 100 },
{ id: 2, name: '商品B', quantity: 2, price: 200 }
]);
// 2. 计算总价(响应式:依赖cart的变化)
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.quantity * item.price, 0);
});
// 3. 增加数量:用splice修改数组(遵循官网推荐)
function increment(index) {
const item = cart.value[index];
// 生成新商品对象(避免直接修改原对象)
const newItem = { ...item, quantity: item.quantity + 1 };
// 用splice替换原商品(触发响应式)
cart.value.splice(index, 1, newItem);
}
// 4. 减少数量:用splice修改数组
function decrement(index) {
const item = cart.value[index];
if (item.quantity > 1) {
const newItem = { ...item, quantity: item.quantity - 1 };
cart.value.splice(index, 1, newItem);
}
}
// 5. 删除商品:用splice修改数组
function removeItem(index) {
cart.value.splice(index, 1);
}
// 6. 添加商品:用push修改数组
function addItem() {
const newItem = {
id: Date.now(), // 用时间戳作为唯一ID
name: `商品${cart.value.length + 1}`,
quantity: 1,
price: Math.floor(Math.random() * 100) + 50 // 随机单价(50-149元)
};
cart.value.push(newItem); // push是变异方法,触发响应式
}
</script>
<style scoped>
.cart { max-width: 400px; margin: 20px auto; padding: 20px; border: 1px solid #eee; }
.cart-item { display: flex; align-items: center; margin: 10px 0; }
.cart-item span { margin: 0 10px; }
.total { font-weight: bold; margin: 20px 0; }
.add-btn { padding: 8px 16px; background: #42b983; color: #fff; border: none; border-radius: 4px; }
</style>
3. 代码说明
ref包裹数组 :cart是ref对象,cart.value是响应式数组,修改时需通过.value。- 变异方法的使用 :
splice(修改数量、删除商品)、push(添加商品)都是数组变异方法,确保响应式。 - 计算属性
computed:totalPrice依赖cart的变化,自动更新总价,无需手动触发。
课后Quiz:测试你的理解
问题1
用reactive创建的对象const user = reactive({ name: 'Alice' }),如何新增响应式属性age?(至少2种方法)
问题2
用ref包裹的数组const list = ref([1,2,3]),如何修改索引0的值为4?(至少2种方法)
答案解析
问题1答案:
- 使用
set函数:set(user, 'age', 20)。 - 扩展运算符生成新对象:
const userRef = ref(user); userRef.value = { ...userRef.value, age: 20 }(reactive对象不能直接赋值,需用ref包裹)。
问题2答案:
- 用
splice:list.value.splice(0, 1, 4)。 - 用
set函数:set(list.value, 0, 4)。 - 直接修改(最新版本支持):
list.value[0] = 4。
常见报错解决方案
报错1:value cannot be made reactive: 0
- 原因 :
reactive无法代理原始值(如number)。 - 解决 :用
ref代替reactive:const count = ref(0)。
报错2:Cannot add property age, object is not extensible
- 原因 :
reactive对象被冻结(Object.freeze),无法新增属性。 - 解决 :不要冻结响应式对象,或用
ref包裹后修改.value。
报错3:TypeError: Cannot read properties of undefined (reading 'value')
- 原因 :
ref对象未初始化,或模板中错误使用.value。 - 解决 :确保
ref初始值非undefined,模板中直接用变量名(如{{ count }}而非{{ count.value }})。
预防报错的建议
- 原始值用
ref:reactive只代理对象/数组,原始值优先用ref。 - 数组用变异方法 :修改数组时,优先用
push、splice等变异方法,避免直接修改索引。 - 对象新增属性用
set:新增对象属性时,用set函数而非直接赋值。
参考链接
- Vue3官网"响应式系统的局限性":vuejs.org/guide/essen...
- Vue3官网"Ref API":vuejs.org/api/reactiv...
- Vue3官网"数组变异方法":vuejs.org/guide/essen...