前言
在一个典型的搜索框实现中,用户每输入一个字符,前端应用都会向服务器发送一个请求,获取与当前输入匹配的搜索建议。
然而,由于网络延迟和服务器处理时间的不可预测性,请求的返回顺序可能与发送顺序不一致。
例如,用户在短时间内快速输入多个字符(如从“a”到“ab”到“abc”),服务器可能会先返回后一个请求(“ab”),再返回前一个请求(“a”)。如果不作处理,用户看到的搜索建议列表可能会与其输入的内容不匹配,造成不良的用户体验。
原因分析
- 网络延迟:每个请求在网络中传输的时间是不可控的,可能会受到网络状态、服务器负载等因素的影响,从而导致延迟的差异。
- 服务器处理时间:服务器处理每个请求的时间可能不同。一些请求可能涉及更复杂的计算或数据库查询,从而花费更多的时间。
- 请求频率过高:当用户快速输入多个字符时,前端可能会在短时间内发送大量请求。这些请求同时到达服务器,服务器需要排队处理,从而进一步增加了响应的不确定性。
为什么单独使用防抖和节流无法解决问题
在深入解决方案之前,我们先来看看两个常见的前端优化技术:防抖(Debouncing) 和 节流(Throttling),以及它们各自的局限性。
防抖(Debouncing)
防抖是一种确保在特定时间内只触发一次操作的技术。在搜索建议中,防抖意味着只有用户停止输入一段时间后才发送请求。这可以减少请求的频率,避免服务器负载过重。
例如,我们设置防抖时间为300ms,当用户输入“123”时,只有在输入“3”后的300ms内没有其他输入时,才会发送请求获取“123”的建议。这种方法在很多场景下非常有效,但在我们的场景中可能会有问题:
- 用户输入“1”时,请求1发送出去,此时用户立即输入“2”。
- 请求1在网络中传输,用户输入“2”后等待了一会,请求2发送出去。
- 由于网络延迟,请求1可能在请求2之后返回。
图
这种情况下,即便使用防抖,仍然可能出现旧请求的响应覆盖新请求的问题,因为请求1的响应可能比请求2、3更晚返回。
节流(Throttling)
节流是指在一定时间内最多只能执行一次操作。节流可以确保在用户输入过程中,应用不会频繁地向服务器发送请求,减少服务器的压力。
然而,节流与防抖一样,也不能完全解决请求返回顺序的问题。节流只控制了请求的发送频率,但无法确保响应的顺序。例如:
- 用户输入“1”,应用在节流时间内发送了请求1。
- 用户继续输入“2”和“3”,节流时间内没有发送新请求。
- 但由于网络问题,请求1的响应可能在用户输入“3”之后返回,仍然导致不一致的问题。
解决方案
使用 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>