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

Vue 3 响应式系统深度探索:构建购物车应用 - 精通 `watch` 和 `computed` 的响应式数据

引言

欢迎再次回到 Vue 3 + 现代前端工程化 系列技术博客! 在昨天的第二篇博客中,我们深入学习了 Vue 3 的组件化开发,并构建了一个实用的待办事项列表应用,掌握了组件创建、数据传递和事件通信等关键技能。 今天,我们将继续深入 Vue 3 的核心,聚焦于响应式系统的更高级用法,特别是 watch 监听器和 computed 计算属性。

响应式系统是 Vue 3 的灵魂,它赋予了 Vue 3 数据驱动视图的能力。 理解响应式系统不仅仅是掌握 refreactive 两个 API,更重要的是学会如何利用它来构建复杂的数据逻辑和用户界面。 在本篇博客中,我们将通过构建一个 简易购物车应用,深入探索 watchcomputed 的应用场景和最佳实践,进一步提升您对 Vue 3 响应式系统的理解和运用能力。

通过这个项目,您将学习到:

  • Vue 3 响应式系统进阶: 深入理解 watch 监听器和 computed 计算属性的作用和使用场景。
  • watch 监听器: 掌握 watch 的基本用法和高级选项,学会监听数据变化并执行副作用操作。
  • computed 计算属性: 掌握 computed 的基本用法和缓存机制,学会派生计算新的响应式数据。
  • 构建更复杂 UI: 体验如何使用 watchcomputed 构建包含复杂数据逻辑和动态计算的用户界面。
  • 组件内状态管理: 学习在组件内部使用响应式系统管理组件状态,构建更智能化的组件。
  • 技能跃迁: 从响应式数据基础应用进阶到 watchcomputed 的灵活运用,提升您的 Vue 3 状态管理和数据处理能力。

项目简介: 简易购物车应用

我们的简易购物车应用将围绕以下核心功能展开:

  • 商品列表展示: 展示商品列表,包括商品名称和价格。
  • 添加商品到购物车: 用户可以点击按钮将商品添加到购物车。
  • 购物车展示: 展示购物车列表,包括商品名称、数量和小计。
  • 购物车总价计算: 实时计算购物车中所有商品的总价。
  • 从购物车移除商品: 用户可以从购物车中移除已添加的商品。

通过构建购物车应用,我们将深入实践:

  • refreactive: 继续使用 refreactive 创建响应式数据,管理商品列表和购物车数据。
  • computed 计算属性: 使用 computed 计算购物车总价,实现动态计算和缓存优化。
  • watch 监听器: 使用 watch 监听购物车数据变化,模拟商品数量更新或其他副作用操作 (例如,日志记录)。
  • 列表渲染和事件处理: 继续练习使用 v-for 指令和 @click 事件监听器构建交互式用户界面。

Vue 3 响应式系统核心概念进阶回顾

在开始构建购物车应用之前,让我们回顾并拓展 Vue 3 响应式系统的核心概念,重点理解 watch 监听器和 computed 计算属性的作用和区别。

  • 响应式数据 (Ref 和 Reactive): 回顾 refreactive 的基本用法,它们是创建响应式数据的核心 API,用于追踪数据变化并驱动视图更新。 ref 适用于基本类型,reactive 适用于复杂类型 (对象和数组)。
  • watch 监听器: watch 允许我们监听一个或多个响应式数据源,并在数据源发生变化时执行自定义的回调函数 (副作用)。 watch 主要用于执行副作用操作,例如:
    • 数据请求: 当监听的数据变化时,触发 API 请求。
    • 日志记录: 监听数据的变化,记录日志信息。
    • DOM 操作: 监听数据变化,手动操作 DOM 元素 (通常不推荐直接操作 DOM,但某些特殊场景下可能需要)。
    • 更新计算属性依赖的数据: 虽然不常见,watch 也可以用来间接更新 computed 属性依赖的数据。
  • computed 计算属性: computed 用于声明一个依赖于其他响应式数据的计算属性。 计算属性会根据其依赖的响应式数据自动进行缓存和更新。 computed 主要用于派生新的响应式数据,例如:
    • 列表过滤/排序: 基于原始列表数据,计算出过滤或排序后的新列表。
    • 数据格式化: 将原始数据格式化成更易于展示的形式。
    • 动态计算属性值: 根据其他响应式数据动态计算出新的属性值 (例如,购物车总价)。
    • 缓存机制: computed 具有缓存机制。 只要其依赖的响应式数据没有发生变化,多次访问 computed 属性都会直接返回缓存的结果,而不会重新执行计算函数,从而提高性能。

watchcomputed 的重要性

watchcomputed 是 Vue 3 响应式系统中至关重要的两个补充 API,它们赋予了我们更精细化和更强大的状态管理能力。 理解和掌握 watchcomputed,能够帮助我们:

  • 处理更复杂的数据逻辑: 对于需要监听数据变化并执行副作用操作的场景,watch 是最佳选择。 对于需要派生新的响应式数据的场景,computed 能够简化代码并提高性能。
  • 提高代码可读性和可维护性: watchcomputed 将复杂的逻辑从模板中抽离出来,使得组件的代码更加清晰、易于理解和维护。 代码逻辑更加模块化,易于测试和复用。
  • 优化应用性能: computed 的缓存机制能够避免不必要的计算,提高应用性能。 watch 能够更精确地监听数据变化,避免过度触发副作用操作。
  • 构建更智能化的组件: 通过 watchcomputed,我们可以构建更加智能化的组件,组件能够根据数据的变化自动响应,并执行相应的逻辑,提升用户体验。

实战步骤: 构建购物车应用

接下来,我们将一步步使用 Vue 3 响应式系统 (重点是 watchcomputed) 构建我们的简易购物车应用。

步骤 1: 项目初始化 (沿用 Day 1 项目)

我们将沿用 Day 1 创建的 vue3-counter-app 项目。 如果您还没有创建项目,请参考 Day 1 博客中的步骤进行初始化。 无需创建新的项目,我们将在已有的项目基础上进行开发。

步骤 2: 定义商品数据

首先,我们需要在 App.vue 组件的 <script setup> 中定义一些商品数据,用于模拟商品列表。 我们将使用 reactive 创建一个响应式对象,包含商品列表数据。

修改 src/App.vue 文件,在 <script setup> 中添加以下代码:

import { ref, reactive, computed, watch } from 'vue';

// 商品数据 (使用 reactive 创建响应式对象)
const products = reactive([
  { id: 1, name: 'Vue.js 3 核心教程', price: 59.9, image: 'image/vuejs3.jpg' }, // 可选:添加商品图片
  { id: 2, name: 'JavaScript 高级编程', price: 89.0, image: 'image/javascript.jpg' },
  { id: 3, name: 'CSS 揭秘', price: 75.5, image: 'image/css.jpg' },
  { id: 4, name: 'HTML5 权威指南', price: 68.8, image: 'image/html5.jpg' },
]);

// 购物车数据 (使用 reactive 创建响应式对象)
const cart = reactive({
  items: [] // 购物车商品列表,数组
});

代码解释:

  • import { ref, reactive, computed, watch } from 'vue';: 从 vue 模块中导入 ref, reactive, computed, watch 函数,以便在组件中使用。
  • const products = reactive([...]);: 使用 reactive 创建一个名为 products 的响应式对象,用于存储商品列表数据。 products 是一个数组,每个元素都是一个商品对象,包含 id, name, priceimage 属性 (可选的商品图片,本篇博客暂不使用图片)。 我们使用 reactive 而不是 ref,因为 products 是一个复杂对象 (数组)。
  • const cart = reactive({ items: [] });: 使用 reactive 创建一个名为 cart 的响应式对象,用于存储购物车数据。 cart 对象包含一个 items 属性,items 是一个数组,用于存储购物车中的商品。 同样,我们使用 reactive 因为 cart 也是一个复杂对象。

步骤 3: 展示商品列表

接下来,我们需要在模板中展示商品列表。 使用 v-for 指令遍历 products 响应式数据,并渲染商品信息。

修改 App.vue 文件的 <template> 部分,添加商品列表展示代码:

<template>
  <div class="container">
    <h1>简易购物车</h1>

    <h2>商品列表</h2>
    <ul class="product-list">
      <li v-for="product in products" :key="product.id" class="product-item">
        <div class="product-info">
          <h3>{{ product.name }}</h3>
          <p>价格: ¥{{ product.price.toFixed(2) }}</p>
        </div>
        <button @click="addToCart(product)">加入购物车</button>
      </li>
    </ul>

    <h2>购物车</h2>
    <div v-if="cart.items.length === 0">
      <p>购物车空空如也</p>
    </div>
    <ul v-else class="cart-list">
      <li v-for="(item, index) in cart.items" :key="index" class="cart-item">
        <div class="cart-item-info">
          <p>{{ item.product.name }} x {{ item.quantity }}</p>
        </div>
        <button @click="removeFromCart(index)">移除</button>
      </li>
      <li class="cart-total">
        <p>总价: ¥{{ cartTotal }}</p>
      </li>
    </ul>
  </div>
</template>

<script setup>
  // ... (之前的 <script setup> 代码,包括商品数据和购物车数据)

  // 添加商品到购物车的方法
  const addToCart = (product) => {
    //  TODO:  步骤 4 实现
  };

  // 从购物车移除商品的方法
  const removeFromCart = (index) => {
    // TODO: 步骤 6 实现
  };

  // 计算购物车总价 (computed)
  const cartTotal = computed(() => {
    // TODO: 步骤 5 实现
    return 0; // 占位符,待实现
  });

  // 监听购物车变化 (watch)
  watch(
    () => cart.items, // 监听的数据源:购物车商品列表
    (newCartItems, oldCartItems) => { // 回调函数
      console.log('购物车商品列表发生变化:', newCartItems, oldCartItems); // 打印日志
      // TODO:  步骤 7 可选实现其他副作用操作,例如更新本地存储、发送统计数据等
    },
    { deep: true } // 深度监听,因为监听的是数组内部元素的变化
  );
</script>

<style scoped>
/* 组件样式省略,与之前的类似,可以根据需要自行添加样式 */
.container {
  font-family: sans-serif;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.product-list, .cart-list {
  list-style: none;
  padding: 0;
  width: 100%;
  max-width: 600px;
  margin-bottom: 20px;
}

.product-item, .cart-item, .cart-total {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #ccc;
}

.product-info, .cart-item-info {
  flex-grow: 1;
}

.product-item button, .cart-item button {
  padding: 8px 15px;
  cursor: pointer;
}

.cart-total {
  font-weight: bold;
  border-top: 2px solid #ccc;
  border-bottom: none;
  text-align: right;
}
</style>

代码解释:

  • 商品列表展示:

    • <h2>商品列表</h2>: 设置商品列表标题。
    • <ul class="product-list">: 商品列表容器。
    • <li v-for="product in products" ... class="product-item">: 使用 v-for 指令遍历 products 响应式数据,为每个商品渲染一个 <li> 元素。
      • :key="product.id": 绑定 key 属性,用于 Vue 3 追踪列表项的身份。
      • <div class="product-info">: 商品信息容器,包含商品名称和价格。
      • <h3>{{ product.name }}</h3>: 显示商品名称,使用插值语法绑定 product.name
      • <p>价格: ¥{{ product.price.toFixed(2) }}</p>: 显示商品价格,使用插值语法绑定 product.price,并使用 toFixed(2) 方法格式化价格,保留两位小数。
      • <button @click="addToCart(product)">加入购物车</button>: “加入购物车” 按钮,点击时触发 addToCart(product) 方法,并将当前商品 product 作为参数传递。 addToCart 方法将在步骤 4 中实现。
  • 购物车展示:

    • <h2>购物车</h2>: 设置购物车标题。
    • <div v-if="cart.items.length === 0">: 使用 v-if 指令判断购物车是否为空,如果为空,显示提示信息 “购物车空空如也”。
    • <ul v-else class="cart-list">: 购物车列表容器,当购物车不为空时显示。
    • <li v-for="(item, index) in cart.items" ... class="cart-item">: 使用 v-for 指令遍历 cart.items 响应式数据 (购物车商品列表),为每个购物车商品渲染一个 <li> 元素。
      • :key="index": 绑定 key 属性,使用索引 index 作为 key,因为购物车中的商品可能重复。
      • <div class="cart-item-info">: 购物车商品信息容器,包含商品名称和数量。
      • <p>{{ item.product.name }} x {{ item.quantity }}</p>: 显示购物车商品名称和数量,使用插值语法绑定 item.product.nameitem.quantity
      • <button @click="removeFromCart(index)">移除</button>: “移除” 按钮,点击时触发 removeFromCart(index) 方法,并将当前购物车商品在 cart.items 数组中的索引 index 作为参数传递。 removeFromCart 方法将在步骤 6 中实现。
    • <li class="cart-total">: 购物车总价显示容器。
      • <p>总价: ¥{{ cartTotal }}</p>: 显示购物车总价,使用插值语法绑定 cartTotal 计算属性。 cartTotal 计算属性将在步骤 5 中实现。
  • addToCart, removeFromCart, cartTotal, watch: 在 <script setup> 中定义了 addToCart, removeFromCart 方法、 cartTotal 计算属性和 watch 监听器,但其具体实现将在后续步骤中完成 (以 TODO: 注释标记)。

步骤 4: 实现 “加入购物车” 功能 (addToCart 方法)

现在,我们需要实现 addToCart 方法,将商品添加到购物车。 在 addToCart 方法中,我们将检查商品是否已在购物车中,如果已在则增加数量,否则添加新的购物车商品项。

修改 App.vue 文件的 <script setup> 部分,完善 addToCart 方法:

const addToCart = (product) => {
  const cartItem = cart.items.find(item => item.product.id === product.id); // 查找购物车中是否已存在该商品

  if (cartItem) {
    // 如果已存在,增加商品数量
    cartItem.quantity++;
  } else {
    // 如果不存在,添加新的购物车商品项
    cart.items.push({ product: product, quantity: 1 });
  }
};

代码解释:

  • const cartItem = cart.items.find(item => item.product.id === product.id);: 使用 find 方法在 cart.items 数组中查找是否已存在相同 product.id 的购物车商品项。 find 方法返回找到的第一个符合条件的元素,如果未找到则返回 undefined
  • if (cartItem) { ... } else { ... }: 判断 cartItem 是否存在 (是否为 undefined),根据判断结果执行不同的逻辑。
    • if (cartItem): 如果 cartItem 存在 (购物车中已存在该商品),则执行增加商品数量的逻辑。
      • cartItem.quantity++;: 将找到的 cartItemquantity 属性值加 1,增加商品数量。 由于 cartcart.items 都是响应式数据,修改 cartItem.quantity 会自动触发视图更新,购物车列表中对应商品的数量会实时更新。
    • else: 如果 cartItem 不存在 (购物车中不存在该商品),则执行添加新的购物车商品项的逻辑。
      • cart.items.push({ product: product, quantity: 1 });: 使用 push 方法向 cart.items 数组中添加一个新的购物车商品项。 新的购物车商品项是一个对象,包含 product (当前点击的商品对象) 和 quantity: 1 (初始数量为 1)。 由于 cartcart.items 都是响应式数据,向 cart.items 数组添加新的元素会自动触发视图更新,购物车列表中会显示新增的商品项。

步骤 5: 实现购物车总价计算 (cartTotal 计算属性)

接下来,我们需要实现 cartTotal 计算属性,用于动态计算购物车中所有商品的总价。 cartTotal 计算属性将依赖于 cart.items 响应式数据,当 cart.items 发生变化时,cartTotal 会自动重新计算。

修改 App.vue 文件的 <script setup> 部分,完善 cartTotal 计算属性:

const cartTotal = computed(() => {
  let total = 0;
  for (const item of cart.items) {
    total += item.product.price * item.quantity;
  }
  return total.toFixed(2); // 保留两位小数
});

代码解释:

  • const cartTotal = computed(() => { ... });: 使用 computed 函数创建一个名为 cartTotal 的计算属性。 computed 接收一个回调函数作为参数,回调函数的返回值就是计算属性的值。 computed 会自动追踪回调函数中使用的响应式数据依赖。 在本例中,回调函数使用了 cart.items 响应式数据,因此 cartTotal 计算属性依赖于 cart.items
  • let total = 0;: 在回调函数内部,初始化一个 total 变量,用于累加购物车总价。
  • for (const item of cart.items) { ... }: 使用 for...of 循环遍历 cart.items 数组。
    • total += item.product.price * item.quantity;: 对于每个购物车商品项 item,将其商品价格 item.product.price 乘以商品数量 item.quantity,累加到 total 变量中。
  • return total.toFixed(2);: 循环结束后,返回计算出的总价 total,并使用 toFixed(2) 方法格式化价格,保留两位小数。 computed 会将该返回值作为 cartTotal 计算属性的值。

步骤 6: 实现 “从购物车移除商品” 功能 (removeFromCart 方法)

接下来,我们需要实现 removeFromCart 方法,用于从购物车中移除指定索引的商品。

修改 App.vue 文件的 <script setup> 部分,完善 removeFromCart 方法:

const removeFromCart = (index) => {
  cart.items.splice(index, 1); // 从购物车商品列表中移除指定索引的商品项
};

代码解释:

  • const removeFromCart = (index) => { ... };: 定义 removeFromCart 方法,接收一个参数 index,表示要移除的购物车商品项在 cart.items 数组中的索引。
  • cart.items.splice(index, 1);: 使用 splice 方法从 cart.items 数组中删除指定索引 index 的元素,删除数量为 1。 由于 cartcart.items 都是响应式数据,修改 cart.items 数组会自动触发视图更新,购物车列表中被移除的商品项会实时消失。

步骤 7: 使用 watch 监听购物车变化 (可选)

最后,我们使用 watch 监听购物车数据 cart.items 的变化,并在控制台打印日志信息,演示 watch 监听器的基本用法。 您也可以根据实际需求,在 watch 的回调函数中执行其他副作用操作,例如更新本地存储、发送统计数据等 (本篇博客仅演示日志记录)。

修改 App.vue 文件的 <script setup> 部分,完善 watch 监听器 (已在步骤 3 中添加了基本结构,此处只需完善回调函数内部逻辑):

watch(
  () => cart.items, // 监听的数据源:购物车商品列表
  (newCartItems, oldCartItems) => { // 回调函数
    console.log('购物车商品列表发生变化:', newCartItems, oldCartItems); // 打印日志
    // TODO:  可选实现其他副作用操作,例如更新本地存储、发送统计数据等
  },
  { deep: true } // 深度监听,因为监听的是数组内部元素的变化
);

代码解释:

  • watch( ... ): 使用 watch 函数创建一个监听器。
    • () => cart.items: 第一个参数是监听的数据源。 我们使用一个箭头函数返回 cart.items,表示监听 cart.items 响应式数据。 当 cart.items 数组本身或数组内部的元素发生变化时,watch 监听器会触发。
    • (newCartItems, oldCartItems) => { ... }: 第二个参数是回调函数。 当监听的数据源发生变化时,回调函数会被执行。 回调函数接收两个参数:
      • newCartItems: 变化后的新的购物车商品列表。
      • oldCartItems: 变化前的旧的购物车商品列表 (首次执行回调函数时,oldCartItemsundefined)。
      • console.log('购物车商品列表发生变化:', newCartItems, oldCartItems);: 在回调函数内部,使用 console.log 打印日志信息,显示购物车商品列表的变化情况。 您可以根据实际需求,在此处执行其他副作用操作。
    • { deep: true }: 第三个参数是可选的配置项对象。 deep: true 表示开启深度监听。 由于我们监听的是 cart.items 数组,并且需要监听数组内部元素的变化 (例如,修改购物车商品项的数量),因此需要开启深度监听。 如果监听的数据源是基本类型或浅层对象,则不需要开启深度监听。

步骤 8: 运行应用并查看效果

保存 App.vue 文件后,Vite 开发服务器会自动热更新。 切换到浏览器,您将看到一个简易购物车应用。

尝试点击商品列表中的 “加入购物车” 按钮,商品会被添加到购物车中。 购物车列表会实时更新,显示新添加的商品,并且购物车总价也会自动计算并显示。 您可以多次点击 “加入购物车” 按钮,添加多个商品,或者添加相同商品多次,观察购物车列表和总价的变化。 打开浏览器的开发者工具控制台 (Console),您会看到 watch 监听器打印的日志信息,显示购物车商品列表的变化。 点击购物车商品项后面的 “移除” 按钮,可以从购物车中移除商品,购物车列表和总价也会实时更新。

Vue 3 响应式系统进阶优势展现 - watchcomputed 的强大力量

通过构建购物车应用,我们深入体验了 Vue 3 响应式系统 watchcomputed 的强大优势:

  • 数据驱动: 购物车应用的各个部分 (商品列表、购物车列表、购物车总价) 都由响应式数据驱动。 当数据发生变化时,视图会自动更新,无需手动操作 DOM。
  • 自动化计算: 使用 computed 计算属性 cartTotal,实现了购物车总价的动态计算和自动更新。 只要购物车商品列表 cart.items 发生变化,cartTotal 就会自动重新计算,并更新视图,保证总价始终与购物车商品列表同步。 computed 的缓存机制也提高了性能,避免了不必要的重复计算。
  • 副作用处理: 使用 watch 监听器监听购物车商品列表 cart.items 的变化,并在回调函数中打印日志信息,演示了 watch 处理副作用操作的能力。 在实际项目中,watch 可以用于更复杂的副作用操作,例如数据持久化、远程数据同步、复杂的业务逻辑处理等。
  • 更清晰的代码逻辑: computedwatch 将复杂的计算逻辑和副作用操作从模板中抽离出来,使得组件的代码结构更加清晰,逻辑更加内聚,易于理解和维护。

总结与反思

在今天的博客中,我们一起完成了 Vue 3 系列的第三篇学习实践,并成功构建了一个简易购物车应用。 在这个过程中,我们:

  • 深入理解了 Vue 3 响应式系统的进阶用法,重点学习了 watch 监听器和 computed 计算属性的作用和使用场景。
  • 掌握了 watch 监听器的基本用法和深度监听选项,学会了监听数据变化并执行副作用操作。
  • 掌握了 computed 计算属性的基本用法和缓存机制,学会了派生计算新的响应式数据。
  • 体验了如何使用 watchcomputed 构建包含复杂数据逻辑和动态计算的用户界面。

明天,我们将继续深入 Vue 3 的学习,开始探索 Vue 3 的路由管理。 我们将为博客添加多页面导航,学习 Vue Router 的配置和使用,实现页面跳转和参数传递,构建更完善的单页应用 (SPA)。


http://www.kler.cn/a/564948.html

相关文章:

  • Nginx系列05(负载均衡、动静分离)
  • 学习笔记05——HashMap实现原理及源码解析(JDK8)
  • 小程序中头像昵称填写
  • StableDiffusion本地部署 2
  • react覆盖组件样式,不影响其他地方相同类名的组件
  • 【MySQL】数据库安装
  • Oracle RMAN duplicate 标准化文档
  • WPF高级 | WPF 多线程编程:提升应用性能与响应性
  • Ae 效果详解:粒子运动场
  • 渗透测试【海洋cms V9 漏洞】
  • JSP + Servlet 实现 AJAX(纯JS版)
  • cv2.solvePnP 报错 求相机位姿
  • Storage Gateway:解锁企业混合云存储的智能钥匙
  • Mysql表字段字符集未设置导致乱码问题
  • 构建逻辑思维链(CoT)为金融AI消除幻觉(保险理赔篇)
  • WPF-3天快速WPF入门并达到企业级水准
  • 如何在 UniApp 中集成激励奖励(流量主)
  • Shot Studio for macOS 发布 1.0.2
  • 智能语音机器人为电销行业带来一场革命性的变化
  • Java中字符流和字节流的区别