现在位置: 首页 > Vue3 教程 > 正文

watch 与搜索功能

本章你将学会用 watch 和 watchEffect 监听数据变化,并实现博客搜索框的实时过滤功能。


什么场景需要 watch?

大多数情况下,Vue 的响应式绑定已经足够:数据变了,模板自动更新。

但有些场景需要你在数据变化时主动执行一段逻辑,比如:

  • 搜索框输入后,过滤文章列表
  • 用户切换分类时,更新页面标题
  • 数据变化后,存到 localStorage
  • 表单字段变化时,做表单校验

这时就需要 watchwatchEffect


watch — 精确监听

watch 监听指定的数据源,当数据变化时执行回调函数。

你可以拿到 新值旧值,做对比处理。

实例

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

const keyword = ref('')

// watch(被监听的数据源, 回调函数)
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索关键词从 "${oldVal}" 变成了 "${newVal}"`)
  // 在这里执行过滤逻辑
})
</script>

<template>
  <input v-model="keyword" placeholder="搜索文章...">
  <p>当前搜索:{{ keyword }}</p>
</template>

watch 的三种数据源

数据源类型写法示例
单个 refwatch(ref, callback)watch(keyword, fn)
reactive 对象的属性watch(() => obj.prop, callback)watch(() => user.name, fn)
多个数据源watch([ref1, ref2], callback)watch([kw, cat], fn)

实例

import { ref, watch } from 'vue'

const keyword = ref('')
const category = ref('全部')

// 监听单个 ref
watch(keyword, (newVal) => {
  document.title = newVal ? `搜索:${newVal}` : 'RUNOOB Blog'
})

// 监听多个数据源
watch([keyword, category], ([newKw, newCat], [oldKw, oldCat]) => {
  console.log('关键词或分类变化了,重新过滤文章')
  // 执行过滤逻辑...
})

watch 默认是懒执行的——数据第一次有值时不会触发,只有后续变化才触发。如果需要在初始化时也执行一次,加上 { immediate: true } 配置项。


watchEffect — 自动追踪依赖

watchEffect 不需要手动指定数据源,它会自动追踪回调中用到的响应式数据。

与 watch 最大的区别:watchEffect 会立即执行一次

实例

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

const keyword = ref('')
const articles = ref([
  { id: 1, title: 'Vue3 入门' },
  { id: 2, title: 'JS 异步' },
])

const filteredArticles = ref([])

// watchEffect 自动追踪 keyword 和 articles
// 并且会立即执行一次,初始化 filteredArticles
watchEffect(() => {
  // 函数里用到的所有响应式数据都会被自动追踪
  filteredArticles.value = articles.value.filter(
    a => a.title.includes(keyword.value)
  )
  console.log(`过滤结果:${filteredArticles.value.length} 篇`)
})
</script>

<template>
  <input v-model="keyword" placeholder="搜索">
  <div v-for="a in filteredArticles" :key="a.id">
    {{ a.title }}
  </div>
</template>

watch vs watchEffect 对比

特性watchwatchEffect
是否指定数据源手动指定自动追踪依赖
初次执行默认不执行(可配 immediate)立即执行
获取旧值可以拿到旧值拿不到旧值
适用场景需要比较新旧值、精确控制时机不关心新旧值,只是想「响应变化」

简单选择法:如果你的逻辑是「当 X 变化时,去做 Y」,用 watch。如果你的逻辑是「只要有依赖变了,就重新计算结果」,用 watchEffect 或 computed。实际上,能算出来的数据优先用 computed,watch 用来处理「副作用」——如调 API、存本地、操作 DOM。


防抖思路

搜索框每次按键都会触发过滤,对于本地数据这还好,但如果每次输入都要发网络请求,就会造成大量浪费。

防抖(Debounce) 的思路:等用户停止输入一段时间后,再执行操作。

实例

import { ref, watch } from 'vue'

const keyword = ref('')
const filteredArticles = ref([])

let timer = null   // 定时器 ID

watch(keyword, (newVal) => {
  // 清除上一次的定时器
  clearTimeout(timer)
  // 300ms 内没有新的输入变化,才执行过滤
  timer = setTimeout(() => {
    filteredArticles.value = articles.value.filter(
      a => a.title.includes(newVal) || a.summary.includes(newVal)
    )
  }, 300)
})

对于本博客项目,数据在本地,防抖不是必须的。但了解这个思路对后续大型项目很有帮助。


动手:给博客加上实时搜索

在 NavBar 组件中加入搜索框,输入关键词实时过滤文章标题和摘要。

实例

<!-- 文件路径:src/views/HomeView.vue -->
<script setup>
import { ref, computed, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

const articles = ref([])            // 所有文章
const isLoading = ref(true)
const activeCategory = ref('全部')
const keyword = ref('')             // 搜索关键词

// 提取所有分类
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
  return ['全部', ...new Set(cats)]
})

// computed 会自动追踪 keyword 和 activeCategory 的变化
// 任何一项变了,过滤结果都会自动重新计算
const filteredArticles = computed(() => {
  let result = articles.value

  // 第一步:按分类过滤
  if (activeCategory.value !== '全部') {
    result = result.filter(a => a.category === activeCategory.value)
  }

  // 第二步:按关键词过滤(搜索标题和摘要)
  if (keyword.value.trim()) {
    const kw = keyword.value.trim().toLowerCase()
    result = result.filter(a =>
      a.title.toLowerCase().includes(kw) ||
      a.summary.toLowerCase().includes(kw)
    )
  }

  return result
})

// 切换分类时不清空搜索,二者叠加工作
function handleCategoryChange(cat) {
  activeCategory.value = cat
}

// 加载数据
onMounted(async () => {
  try {
    const res = await fetch('/posts.json')
    articles.value = await res.json()
  } catch (err) {
    console.error('加载失败', err)
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <div class="home">
    <!-- 搜索框 -->
    <div class="search-bar">
      <input
        v-model="keyword"
        type="text"
        placeholder="搜索文章标题或摘要..."
        class="search-input"
      />
      <span v-if="keyword" class="clear-btn" @click="keyword = ''">✕</span>
    </div>

    <!-- 分类筛选 -->
    <CategoryFilter
      :categories="categories"
      :active-category="activeCategory"
      @update-category="handleCategoryChange"
    />

    <!-- 加载 / 空 / 正常 -->
    <p v-if="isLoading">加载中...</p>
    <p v-else-if="filteredArticles.length === 0">没有找到匹配的文章</p>
    <div v-else class="article-grid">
      <BlogCard
        v-for="article in filteredArticles"
        :key="article.id"
        :id="article.id"
        :title="article.title"
        :summary="article.summary"
        :date="article.date"
        :category="article.category"
      />
    </div>
  </div>
</template>

<style scoped>
.home { max-width: 960px; margin: 0 auto; padding: 20px; }

.search-bar {
  position: relative;
  margin-bottom: 20px;
}

.search-input {
  width: 100%;
  padding: 12px 40px 12px 16px;
  border: 2px solid #eee;
  border-radius: 8px;
  font-size: 15px;
  outline: none;
  transition: border-color 0.2s;
}

.search-input:focus {
  border-color: #42b883;
}

.clear-btn {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  color: #999;
  font-size: 18px;
}

.article-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
  margin-top: 20px;
}
</style>

现在搜索和分类筛选可以同时工作——搜索关键词后,再点分类按钮,结果会叠加过滤。两个条件都是响应式的,任意一个变化,filteredArticles 都会自动更新。


本章小结

本章你掌握了 watch(手动指定数据源,可拿到新旧值)和 watchEffect(自动追踪依赖,立即执行)的用法,并学会了用 computed 实现搜索 + 分类的叠加过滤。

搜索框的实时过滤体验比「输入完点按钮」流畅很多——这就是响应式系统的价值。