Skip to content

有效管理搜索建议组件的请求

Published:

前言

在一个典型的搜索框实现中,用户每输入一个字符,前端应用都会向服务器发送一个请求,获取与当前输入匹配的搜索建议。

然而,由于网络延迟和服务器处理时间的不可预测性,请求的返回顺序可能与发送顺序不一致。

例如,用户在短时间内快速输入多个字符(如从“a”到“ab”到“abc”),服务器可能会先返回后一个请求(“ab”),再返回前一个请求(“a”)。如果不作处理,用户看到的搜索建议列表可能会与其输入的内容不匹配,造成不良的用户体验。

原因分析

  1. 网络延迟:每个请求在网络中传输的时间是不可控的,可能会受到网络状态、服务器负载等因素的影响,从而导致延迟的差异。
  2. 服务器处理时间:服务器处理每个请求的时间可能不同。一些请求可能涉及更复杂的计算或数据库查询,从而花费更多的时间。
  3. 请求频率过高:当用户快速输入多个字符时,前端可能会在短时间内发送大量请求。这些请求同时到达服务器,服务器需要排队处理,从而进一步增加了响应的不确定性。

为什么单独使用防抖和节流无法解决问题

在深入解决方案之前,我们先来看看两个常见的前端优化技术:防抖(Debouncing)节流(Throttling),以及它们各自的局限性。

防抖(Debouncing)

防抖是一种确保在特定时间内只触发一次操作的技术。在搜索建议中,防抖意味着只有用户停止输入一段时间后才发送请求。这可以减少请求的频率,避免服务器负载过重。

例如,我们设置防抖时间为300ms,当用户输入“123”时,只有在输入“3”后的300ms内没有其他输入时,才会发送请求获取“123”的建议。这种方法在很多场景下非常有效,但在我们的场景中可能会有问题:

这种情况下,即便使用防抖,仍然可能出现旧请求的响应覆盖新请求的问题,因为请求1的响应可能比请求2、3更晚返回。

节流(Throttling)

节流是指在一定时间内最多只能执行一次操作。节流可以确保在用户输入过程中,应用不会频繁地向服务器发送请求,减少服务器的压力。

然而,节流与防抖一样,也不能完全解决请求返回顺序的问题。节流只控制了请求的发送频率,但无法确保响应的顺序。例如:

解决方案

使用 AbortController 取消未完成的请求

思路:在每次新请求之前,使用AbortController来取消之前未完成的请求。这可以确保只有最新的请求会被处理,从而避免旧请求的响应覆盖新请求的结果。

<template>
  <input v-model="searchTerm" placeholder="Search..." />
  <ul>
    <li v-for="suggestion in suggestions" :key="suggestion.id">
      {{ suggestion.name }}
    </li>
  </ul>
</template>

<script lang="ts">
import { ref, watch } from "vue";

export default {
  setup() {
    const searchTerm = ref("");
    const suggestions = ref([]);
    let debounceTimeout: ReturnType<typeof setTimeout>;
    let controller: AbortController | null = null;

    // 监听搜索词变化
    watch(searchTerm, newTerm => {
      if (debounceTimeout) {
        clearTimeout(debounceTimeout);
      }

      debounceTimeout = setTimeout(() => {
        if (controller) {
          controller.abort(); // 取消前一个请求
        }
        controller = new AbortController();
        fetchSuggestions(newTerm, controller.signal);
      }, 300); // 300ms 的防抖时间
    });

    const fetchSuggestions = async (query: string, signal: AbortSignal) => {
      try {
        const response = await fetch(`/api/suggestions?query=${query}`, {
          signal,
        });
        if (response.ok) {
          suggestions.value = await response.json();
        }
      } catch (error) {
        if (error.name === "AbortError") {
          console.log("Request aborted");
        } else {
          console.error("Fetch error:", error);
        }
      }
    };

    return {
      searchTerm,
      suggestions,
    };
  },
};
</script>

使用时间戳判断响应有效性

思路:在发送每个请求时,为请求添加一个时间戳,记录下当前时间。在收到响应时,比较这个时间戳和最新的请求时间戳,只有当响应的时间戳等于或大于最新请求的时间戳时,才更新搜索建议。

<template>
  <input v-model="searchTerm" placeholder="Search..." />
  <ul>
    <li v-for="suggestion in suggestions" :key="suggestion.id">
      {{ suggestion.name }}
    </li>
  </ul>
</template>

<script lang="ts">
import { ref, watch } from "vue";

export default {
  setup() {
    const searchTerm = ref("");
    const suggestions = ref([]);
    let lastRequestTime = 0;

    // 监听搜索词变化
    watch(searchTerm, newTerm => {
      const requestTime = Date.now();
      lastRequestTime = requestTime;
      fetchSuggestions(newTerm, requestTime);
    });

    const fetchSuggestions = async (query: string, requestTime: number) => {
      const response = await fetch(`/api/suggestions?query=${query}`);
      if (response.ok) {
        const result = await response.json();
        if (requestTime >= lastRequestTime) {
          suggestions.value = result;
        }
      }
    };

    return {
      searchTerm,
      suggestions,
    };
  },
};
</script>

结合防抖和时间戳

结合防抖和时间戳可以进一步优化,既减少请求频率,又确保请求和响应的顺序。

<template>
  <input v-model="searchTerm" placeholder="Search..." />
  <ul>
    <li v-for="suggestion in suggestions" :key="suggestion.id">
      {{ suggestion.name }}
    </li>
  </ul>
</template>

<script lang="ts">
import { ref, watch } from "vue";

export default {
  setup() {
    const searchTerm = ref("");
    const suggestions = ref([]);
    let debounceTimeout: ReturnType<typeof setTimeout>;
    let lastRequestTime = 0;

    // 监听搜索词变化
    watch(searchTerm, newTerm => {
      if (debounceTimeout) {
        clearTimeout(debounceTimeout);
      }

      debounceTimeout = setTimeout(() => {
        const requestTime = Date.now();
        lastRequestTime = requestTime;
        fetchSuggestions(newTerm, requestTime);
      }, 300); // 300ms 的防抖时间
    });

    const fetchSuggestions = async (query: string, requestTime: number) => {
      const response = await fetch(`/api/suggestions?query=${query}`);
      if (response.ok) {
        const result = await response.json();
        if (requestTime >= lastRequestTime) {
          suggestions.value = result;
        }
      }
    };

    return {
      searchTerm,
      suggestions,
    };
  },
};
</script>

参考资料