在前端开发中,搜索框的实时搜索功能是一个常见场景。用户输入文字时,每输入一个字符就发起搜索请求。但这里存在一个潜在的竞态问题:如果后发起的请求比先发起的请求更快返回,可能会导致错误的搜索结果显示。
防抖能一定程度减少竞态问题的出现,但是无法杜绝。
竞态问题示例
假设用户快速输入"ab":
- 输入"a"时发起请求A
- 输入"b"时发起请求B
- 请求B先返回,显示"ab"的结果
- 请求A后返回,错误地覆盖为"a"的结果
乐观锁解决方案
我们可以借鉴后端乐观锁的机制来解决这个问题:
复现竞态问题示例
加锁解决竞态问题示例
架构图
源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>竞态问题搜索示例</title>
<style>
#results {
margin-top: 20px;
border: 1px solid #ccc;
padding: 10px;
min-height: 100px;
}
</style>
</head>
<body>
<h1>竞态问题搜索示例</h1>
<input type="text" id="searchInput" placeholder="输入搜索内容..." />
<div id="results">搜索结果将显示在这里</div>
<script>
// 全局请求版本号
let requestVersion = 0
// 模拟API请求
async function mockSearchApi(query) {
// 模拟网络延迟:查询越长,响应越快
const delay = 1000 - query.length * 200
await new Promise((resolve) => setTimeout(resolve, delay))
return `查询"${query}"的结果,延迟${delay}ms`
}
// 防竞态的搜索函数
async function raceSafeSearch(query) {
// 获取当前请求版本号
const currentVersion = ++requestVersion
try {
const response = await mockSearchApi(query)
//检查是否是最新请求
if (currentVersion === requestVersion) {
document.getElementById('results').textContent = response
console.log(`显示结果: ${response}`)
} else {
console.log(`忽略过时结果: ${response} (当前版本: ${requestVersion}, 请求版本: ${currentVersion})`)
}
} catch (error) {
console.error('搜索出错:', error)
}
}
// 监听输入事件
document.getElementById('searchInput').addEventListener('input', (e) => {
const query = e.target.value.trim()
query && raceSafeSearch(query)
})
</script>
</body>
</html>
实现原理
-
全局版本号:
requestVersion
是一个递增的计数器,每次发起新请求时递增 -
请求捕获:每次发起请求前,保存当前的
currentVersion
-
响应验证:当响应返回时,检查
currentVersion
是否等于最新的requestVersion
- 如果相等,说明这是最新的请求结果,可以显示
- 如果不相等,说明已有更新的请求发出,忽略此结果
-
优化:实际使用还可以加上防抖,减少不必要的请求丢弃。
总结
通过这种乐观锁机制,确保了只有最后一次请求的结果会被显示,有效解决了前端搜索中的竞态问题。这种方法简单高效,不需要复杂的状态管理,适用于大多数异步场景下的竞态问题处理。