手写一个实时搜索提示的输入框组件
首先模拟一个搜索提示信息数据的请求
export const fetchData = async (url: string, searchTerm: string) => {
const res = []
for (let i = 0; i < 10; i++) {
res.push({ label: searchTerm + i, value: searchTerm + i, })
}
// 时间随机为 500-1000ms
const randomTime = Math.floor(Math.random() * 500) + 500
await new Promise((resolve) => setTimeout(resolve, randomTime))
console.log(url, randomTime)
return res;
}
组件结构非常简单,一个输入框,一个提示列表。在 onInput
事件触发请求,使用响应的数据渲染提示信息列表。
import { useState } from "react"
import { fetchData } from "./fetch-data"
export const SearchBox: React.FC<{}> = () => {
const [searchList, setSearchList] = useState<{ label: string, value: string }[]>([])
const onInput = async (e: any) => {
try {
const res = await fetchData("/api/search", e.target?.value)
setSearchList(res)
} catch (error) {
console.log(error)
}
}
return (
<div className="search-wrapper">
<input type="text" onInput={onInput} />
<ul className="complete-list">
{searchList.map(item => (
<li key={item.value}>
{item.label}
</li>
))}
</ul>
</div>
)
}
由于我们在每次输入都会触发请求,而且响应的时间是不确定的,这种高频的输入变化可能会引起请求的竞态条件(race condition),导致返回的搜索结果与当前搜索框中的查询字符串不匹配
countRef
在这里充当了一个请求计数器的角色,每次触发 onInput
事件时,都会递增这个计数器的值。通过将当前的计数器值 temp
存储在一个局部变量中,然后将其与全局的 countRef.current
比较,这个代码检查了在异步操作完成之前 countRef.current 是否发生了改变。
如果 countRef.current 的值在请求过程中被修改(这说明有新的输入事件发生了),则按钮点击时的查询已不再是最新的,就会直接返回而不设置搜索结果列表。这就确保了只有最后一次输入时触发的搜索请求得到的结果会被渲染到组件上。
const countRef = useRef(0)
const [searchList, setSearchList] = useState<{ label: string, value: string }[]>([])
const onInput = async (e: any) => {
countRef.current += 1
const temp = lockRef.current
try {
const res = await fetchData("/api/search", e.target?.value)
if (countRef.current !== temp) return
setSearchList(res)
} catch (error) {
console.log(error)
}
}
这样一来
- 确保即便是在快速连续输入的情况下也只显示最后一次输入的搜索结果。
- 防止之前的搜索请求覆盖了后来的请求结果。
- 处理并发请求的顺序问题,避免了潜在的竞态条件。