Vue3 Hooks初探:手把手实现筛选面板组件

377 阅读5分钟

Vue3 Hooks初探:手把手实现筛选面板组件

前言

Vue3 的组合式 API(Composition API)带来了更灵活的代码组织方式,其中 Hooks 机制是核心特性之一。本文通过构建一个功能完整的筛选面板组件,从基础概念到实际应用,逐步深入讲解 Vue3 Hooks 的使用技巧,帮助开发者掌握响应式数据、计算属性、生命周期钩子等核心功能。


一、Vue3 Hooks 基础概念

1. 什么是 Hooks?

在 Vue3 中,Hooks 是指通过组合式 API 定义的函数,用于在组件中复用逻辑。Hooks 函数通常以 use 开头命名(如 useStateuseFetch),其本质是封装了响应式数据(ref/reactive)、计算属性(computed)、生命周期钩子(onMounted)等功能的函数。

2. Hooks 的核心特性

  • 模块化:将组件逻辑拆分为独立函数,提升复用性。
  • 响应式:基于 refreactive 实现数据驱动视图更新。
  • 生命周期集成:通过 onMountedonUnmounted 等钩子处理副作用。
  • 组合能力:多个 Hooks 可自由组合,形成复杂逻辑。

二、筛选面板需求分析

我们需要实现一个支持以下功能的筛选面板:

  1. 分类筛选:多选类别(如电子产品、服装)。
  2. 价格区间:滑动条选择最低和最高价格。
  3. 评分筛选:单选按钮选择最低评分。
  4. 实时搜索:输入关键词过滤商品名称。
  5. 结果展示:根据筛选条件动态渲染商品列表。

三、代码实现步骤

1. 环境准备

使用 Vite 创建 Vue3 项目:

npm init vite@latest vue3-filter-panel --template vue
cd vue3-filter-panel
npm install

2. 基础组件结构

创建 FilterPanel.vue 组件,定义基础状态:

<template>
  <div class="filter-panel">
    <!-- 分类筛选 -->
    <div>
      <h3>Categories</h3>
      <div v-for="category in categories" :key="category">
        <input type="checkbox" :id="'cat-' + category" :value="category" v-model="filterState.categories" />
        <label :for="'cat-' + category">{{ category }}</label>
      </div>
    </div>

    <!-- 价格区间 -->
    <div>
      <h3>Price Range</h3>
      <input type="number" v-model.number="filterState.priceRange[0]" placeholder="Min" />
      <input type="number" v-model.number="filterState.priceRange[1]" placeholder="Max" />
    </div>

    <!-- 评分筛选 -->
    <div>
      <h3>Rating</h3>
      <select v-model.number="filterState.minRating">
        <option :value="0">All</option>
        <option value="1">1+ Stars</option>
        <option value="2">2+ Stars</option>
        <option value="3">3+ Stars</option>
        <option value="4">4+ Stars</option>
        <option value="5">5 Stars</option>
      </select>
    </div>

    <!-- 搜索框 -->
    <div>
      <h3>Search</h3>
      <input v-model="filterState.searchQuery" placeholder="Search products..." />
    </div>

    <!-- 结果展示 -->
    <div class="results">
      <h2>Results ({{ filteredProducts.length }})</h2>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Category</th>
            <th>Price</th>
            <th>Rating</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="product in filteredProducts" :key="product.id">
            <td>{{ product.name }}</td>
            <td>{{ product.category }}</td>
            <td>${{ product.price }}</td>
            <td>{{ product.rating }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'

// 原始数据
const products = ref([
  { id: 1, name: 'Smartphone', category: 'Electronics', price: 599, rating: 4.5 },
  { id: 2, name: 'Laptop', category: 'Computers', price: 999, rating: 4.7 },
  { id: 3, name: 'Headphones', category: 'Electronics', price: 199, rating: 4.3 },
  { id: 4, name: 'Coffee Maker', category: 'Home Appliances', price: 89, rating: 4.1 },
  { id: 5, name: 'Sneakers', category: 'Fashion', price: 129, rating: 4.6 }
])

// 筛选条件状态
const filterState = reactive({
  categories: [], // 选中的分类
  priceRange: [0, 1000], // 价格区间
  minRating: 0, // 最低评分
  searchQuery: '' // 搜索关键词
})

// 计算筛选后的商品列表
const filteredProducts = computed(() => {
  return products.value.filter(product => {
    // 分类筛选
    if (filterState.categories.length && !filterState.categories.includes(product.category)) {
      return false
    }
    // 价格区间筛选
    if (product.price < filterState.priceRange[0] || product.price > filterState.priceRange[1]) {
      return false
    }
    // 评分筛选
    if (filterState.minRating > 0 && product.rating < filterState.minRating) {
      return false
    }
    // 搜索关键词匹配(不区分大小写)
    if (filterState.searchQuery && !product.name.toLowerCase().includes(filterState.searchQuery.toLowerCase())) {
      return false
    }
    return true
  })
})
</script>

<style scoped>
.filter-panel { display: flex; flex-wrap: wrap; gap: 20px; }
.filter-panel > div { flex: 1 1 200px; background: #f5f5f5; padding: 16px; border-radius: 8px; }
input[type="number"], select { width: 100%; margin-top: 8px; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
</style>

四、核心功能解析

1. 响应式数据管理

  • ref vs reactive
    • ref 用于单一数据(如 searchQuery)。
    • reactive 用于对象或数组(如 filterState),便于整体响应式追踪。
  • 示例filterState 使用 reactive 定义,所有筛选条件变化均会自动触发计算属性更新。

2. 计算属性(computed

  • 作用:综合所有筛选条件,返回过滤后的数据。
  • 优势:自动缓存计算结果,仅在依赖项变化时重新计算。
  • 示例filteredProducts 通过链式过滤条件生成最终结果。

3. 生命周期钩子

  • onMounted:模拟异步数据加载。
    onMounted(() => {
      // 模拟 API 请求
      setTimeout(() => {
        products.value = [...products.value, { id: 6, name: 'Smartwatch', category: 'Electronics', price: 249, rating: 4.2 }]
      }, 1000)
    })
    
  • onBeforeUnmount:清理定时器,防止内存泄漏。

4. 实时搜索防抖(watch

  • 问题:搜索输入频繁触发计算属性,造成性能浪费。
  • 解决方案:使用 watch 监听 searchQuery,添加防抖处理。
    let searchTimeout = null
    
    watch(
      () => filterState.searchQuery,
      (query) => {
        clearTimeout(searchTimeout)
        searchTimeout = setTimeout(() => {
          console.log('Search query:', query)
          // 可在此调用后端 API
        }, 300)
      },
      { immediate: true }
    )
    

五、优化与扩展

1. 封装自定义 Hooks

将筛选逻辑抽象为可复用的函数:

// useFilter.js
import { reactive, toRefs } from 'vue'

export function useFilter() {
  const state = reactive({
    categories: [],
    priceRange: [0, 1000],
    minRating: 0,
    searchQuery: ''
  })
  return toRefs(state)
}

在组件中使用:

const { categories, priceRange, minRating, searchQuery } = useFilter()

2. 持久化筛选条件

利用 localStorage 保存用户选择:

watch(
  () => filterState,
  (newVal) => { localStorage.setItem('filterState', JSON.stringify(newVal)) },
  { deep: true }
)

onMounted 中恢复状态:

onMounted(() => {
  const saved = JSON.parse(localStorage.getItem('filterState'))
  if (saved) Object.assign(filterState, saved)
})

3. 异步数据加载

products 改为从 API 获取:

const products = ref([])
onMounted(async () => {
  products.value = await fetchDataFromAPI()
})

六、完整代码示例

完整代码已整合至前述 FilterPanel.vue,关键逻辑总结如下:

  1. 响应式状态filterState 管理所有筛选条件。
  2. 计算属性filteredProducts 实现多条件联合过滤。
  3. 生命周期钩子onMounted 模拟异步数据加载。
  4. 防抖处理watch 监听搜索输入,避免频繁计算。
  5. 持久化存储localStorage 保存用户筛选状态。

七、最佳实践总结

  1. 模块化逻辑:将复杂逻辑拆分为独立 Hooks,提升复用性。
  2. 合理使用 reactive:对对象或数组使用 reactive,单一数据用 ref
  3. 生命周期管理:在 onMounted 处理副作用,onBeforeUnmount 清理资源。
  4. 性能优化:对高频操作(如搜索)使用防抖或节流。
  5. 命名规范:Hooks 函数以 use 开头,明确功能意图。

通过本文的筛选面板案例,你可以掌握 Vue3 Hooks 的核心用法,并将其扩展到更复杂的场景中。建议尝试以下练习:

  • 添加排序功能(如按价格升序/降序)
  • 集成分页功能
  • 将筛选逻辑封装为独立 Hooks 并在其他组件中复用