Skip to content

在 Vue3 中使用 debounce 实现搜索框防抖

在现代 Web 应用中,搜索框是高频交互组件。用户每输入一个字符就触发一次 API 请求,不仅浪费资源,还会导致界面卡顿。使用 防抖(Debounce) 技术可以有效解决这一问题。

本文将展示如何在 Vue3 中结合自定义的 debounce 函数,实现一个高性能的防抖搜索功能。

1. 防抖原理回顾

防抖(Debounce) 的核心思想是: 在用户停止操作一段时间后,才执行函数。

例如:

  • 用户输入 "hello",共 5 次 input 事件
  • 每次输入后重置 300ms 计时器
  • 只有在最后一次输入后 300ms 内无新输入,才调用 searchAPI

这能将 5 次请求减少为 1 次,极大提升性能。

2. 自定义 debounce 函数(TypeScript 版)

ts
// src/utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  wait: number
): T & { cancel: () => void } {
  let timeout: ReturnType<typeof setTimeout> | null = null;

  const debounced = function (this: any, ...args: any[]) {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(this, args);
      timeout = null;
    }, wait);
  };

  // 提供取消功能
  debounced.cancel = () => {
    if (timeout) {
      clearTimeout(timeout);
      timeout = null;
    }
  };

  return debounced as T & { cancel: () => void };
}

3. 在 Vue3 组件中使用

示例:防抖搜索组件

text
<!-- SearchBox.vue -->
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { debounce } from '@/utils/debounce';

// 模拟搜索 API
const searchAPI = async (query: string) => {
  console.log('🔍 搜索请求:', query);
  // 模拟网络请求
  await new Promise(resolve => setTimeout(resolve, 500));
  return [`结果1: ${query}`, `结果2: ${query}`];
};

// 搜索关键词
const keyword = ref('');
// 搜索结果
const results = ref<string[]>([]);
// 加载状态
const loading = ref(false);

// 创建防抖函数(300ms)
const debouncedSearch = debounce(async (query: string) => {
  if (!query.trim()) {
    results.value = [];
    loading.value = false;
    return;
  }

  loading.value = true;
  try {
    const data = await searchAPI(query);
    results.value = data;
  } catch (err) {
    console.error('搜索失败', err);
  } finally {
    loading.value = false;
  }
}, 300);

// 监听 keyword 变化,触发防抖搜索
const handleInput = () => {
  debouncedSearch(keyword.value);
};

// 组件卸载前取消未完成的请求(可选)
onBeforeUnmount(() => {
  debouncedSearch.cancel(); // 取消待执行的搜索
});

// 可选:组件挂载后聚焦输入框
onMounted(() => {
  const input = document.getElementById('search-input');
  input?.focus();
});
</script>

<template>
  <div class="search-container">
    <input
      id="search-input"
      v-model="keyword"
      @input="handleInput"
      type="text"
      placeholder="输入关键词搜索..."
      class="search-input"
      :disabled="loading"
    />

    <div v-if="loading" class="loading">搜索中...</div>

    <ul v-if="results.length > 0" class="results">
      <li v-for="(result, index) in results" :key="index">
        {{ result }}
      </li>
    </ul>

    <p v-else-if="!loading && keyword" class="no-results">
      无匹配结果
    </p>
  </div>
</template>

<style scoped>
.search-container {
  max-width: 500px;
  margin: 20px auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.search-input {
  width: 100%;
  padding: 10px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  outline: none;
}

.search-input:focus {
  border-color: #007bff;
}

.loading {
  color: #666;
  font-style: italic;
  margin-top: 10px;
}

.results {
  list-style: none;
  padding: 0;
  margin-top: 10px;
}

.results li {
  padding: 8px;
  background: #f9f9f9;
  border: 1px solid #eee;
  margin: 4px 0;
  border-radius: 4px;
}

.no-results {
  color: #999;
  margin-top: 10px;
}
</style>

4. 关键点解析

为什么使用 @input 而不是 v-model@change

  • @input:每次输入(包括删除、粘贴)都触发,实时响应
  • @change:仅在输入框失去焦点或回车时触发,不适合搜索

为什么 debounce 返回函数而不是直接调用?

ts
const debouncedSearch = debounce(searchAPI, 300)
  • 返回的是一个“包装后”的函数
  • 可以在多个地方复用
  • 支持 cancel() 取消

为什么在 onBeforeUnmount 中调用 cancel()

  • 防止组件销毁后,延迟的搜索回调试图更新已卸载的组件状态
  • 避免内存泄漏和 Cannot read property 'value' of null 错误

5. 进阶优化建议

(1) 结合 watch 实现更简洁的写法

ts
import { watch } from 'vue';

watch(keyword, (newVal) => {
  if (newVal) {
    debouncedSearch(newVal);
  } else {
    results.value = [];
  }
});

(2) 添加取消上一次请求的能力(使用 AbortController

ts
let controller: AbortController | null = null;

const debouncedSearch = debounce(async (query: string) => {
  // 取消上一次请求
  if (controller) controller.abort();
  controller = new AbortController();

  try {
    const res = await fetch(`/api/search?q=${query}`, {
      signal: controller.signal
    });
    const data = await res.json();
    results.value = data;
  } catch (err) {
    if (err.name !== 'AbortError') {
      console.error('搜索失败', err);
    }
  }
}, 300);

6. 效果演示

用户输入序列传统方式防抖方式(300ms)
h → he → hel → hell → hello5 次请求1 次请求("hello")
a → ab → abc → (停顿 500ms) → abcd4 次请求2 次请求("abc", "abcd")

结语:防抖是前端性能优化的基石

在 Vue3 中使用 debounce 实现搜索防抖,不仅能:

  • 减少服务器压力
  • 提升用户体验
  • 降低网络消耗

更重要的是,它体现了你对性能和用户体验的极致追求

当你写下:

ts
const debouncedSearch = debounce(searchAPI, 300)

你不仅是在调用一个工具函数,更是在为用户打造一个流畅、高效、专业的应用。

这才是现代前端开发的真正价值。