watch 与搜索功能
本章你将学会用 watch 和 watchEffect 监听数据变化,并实现博客搜索框的实时过滤功能。
什么场景需要 watch?
大多数情况下,Vue 的响应式绑定已经足够:数据变了,模板自动更新。
但有些场景需要你在数据变化时主动执行一段逻辑,比如:
- 搜索框输入后,过滤文章列表
- 用户切换分类时,更新页面标题
- 数据变化后,存到 localStorage
- 表单字段变化时,做表单校验
这时就需要 watch 和 watchEffect。
watch — 精确监听
watch 监听指定的数据源,当数据变化时执行回调函数。
你可以拿到 新值 和 旧值,做对比处理。
实例
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 的三种数据源
| 数据源类型 | 写法 | 示例 |
|---|---|---|
| 单个 ref | watch(ref, callback) | watch(keyword, fn) |
| reactive 对象的属性 | watch(() => obj.prop, callback) | watch(() => user.name, fn) |
| 多个数据源 | watch([ref1, ref2], callback) | watch([kw, cat], fn) |
实例
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 会立即执行一次。
实例
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 对比
| 特性 | watch | watchEffect |
|---|---|---|
| 是否指定数据源 | 手动指定 | 自动追踪依赖 |
| 初次执行 | 默认不执行(可配 immediate) | 立即执行 |
| 获取旧值 | 可以拿到旧值 | 拿不到旧值 |
| 适用场景 | 需要比较新旧值、精确控制时机 | 不关心新旧值,只是想「响应变化」 |
简单选择法:如果你的逻辑是「当 X 变化时,去做 Y」,用 watch。如果你的逻辑是「只要有依赖变了,就重新计算结果」,用 watchEffect 或 computed。实际上,能算出来的数据优先用 computed,watch 用来处理「副作用」——如调 API、存本地、操作 DOM。
防抖思路
搜索框每次按键都会触发过滤,对于本地数据这还好,但如果每次输入都要发网络请求,就会造成大量浪费。
防抖(Debounce) 的思路:等用户停止输入一段时间后,再执行操作。
实例
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 组件中加入搜索框,输入关键词实时过滤文章标题和摘要。
实例
<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 实现搜索 + 分类的叠加过滤。
搜索框的实时过滤体验比「输入完点按钮」流畅很多——这就是响应式系统的价值。
