ArkTS 搜索旧请求覆盖新结果怎么办:中式美食输入框怎么防止慢请求回写
ArkTS 搜索旧请求覆盖新结果怎么办:中式美食输入框怎么防止慢请求回写
中式美食的搜索框不是等用户输入完才有动作。用户可能连续输入“番”“番茄”“番茄炒蛋”,也可能一边删一边改。如果每次输入都发一次查询,旧请求不一定先回来。最常见的现象是:输入框已经是“番茄炒蛋”,列表却显示“番”的结果。
这个问题不要只靠防抖。防抖能减少请求次数,但不能保证慢请求不会最后回来。我的做法是加一个requestVersion,每次发请求都带版本号,只有最新版本能回写页面。
本文导读
| 问题 | 处理方式 |
|---|---|
| 快速输入后结果乱跳 | 每次搜索递增 requestVersion |
| 慢请求覆盖新结果 | 回写前比较版本号 |
| 清空输入框仍显示旧结果 | 清空时也递增版本并回到默认列表 |
| 搜索失败后页面乱 | 保留关键词,单独显示错误态 |
环境和文件边界
| 项目 | 内容 |
|---|---|
| 开发工具 | DevEco Studio |
| HarmonyOS SDK | API 12,Stage 模型 |
| 搜索页 | entry/src/main/ets/features/search/SearchPage.ets |
| 状态层 | entry/src/main/ets/features/search/SearchViewModel.ets |
| 数据层 | entry/src/main/ets/data/repository/RecipeSearchRepository.ets |
| 本文重点 | ArkTS 搜索请求时序、旧请求拦截、RDB 查询 |
错误写法:谁回来晚谁覆盖
asynconKeywordChange(keyword:string):Promise<void>{this.keyword=keywordthis.loading=truethis.result=awaitthis.repository.search(keyword)this.loading=false}这段代码最大的问题是没有判断“这个结果是不是当前输入框想要的结果”。请求 A 搜“番”,请求 B 搜“番茄炒蛋”。如果 B 先回来,页面显示正确;A 后回来,又把列表覆盖成旧结果。
用 requestVersion 拦住旧回写
exportclassSearchViewModel{privaterequestVersion:number=0keyword:string=''loading:boolean=falseerrorText:string=''result:RecipeCard[]=[]asyncsearch(keyword:string):Promise<void>{constversion=++this.requestVersionthis.keyword=keywordthis.loading=truethis.errorText=''try{constrows=awaitthis.repository.searchRecipe(keyword)if(version!==this.requestVersion)returnthis.result=rows}finally{if(version===this.requestVersion){this.loading=false}}}}这个版本号不是为了炫技,就是一个很朴素的判断:你回来时,如果已经不是最新请求,就不要改页面。
清空输入框也要当成一次新请求
asyncclearKeyword():Promise<void>{constversion=++this.requestVersionthis.keyword=''this.errorText=''this.loading=falsethis.result=awaitthis.repository.queryDefaultRecommend()if(version!==this.requestVersion){return}}清空输入框容易被忽略。用户点清空后,旧搜索请求如果还在路上,回来以后会把默认推荐覆盖掉。所以清空也要递增版本。
Repository 只负责查,不负责决定能不能回写
exportclassRecipeSearchRepository{asyncsearchRecipe(keyword:string):Promise<RecipeCard[]>{constkey=keyword.trim()constpredicates=newrelationalStore.RdbPredicates('recipe')predicates.like('search_text',`%${key}%`)predicates.orderByDesc('score')predicates.orderByDesc('updated_at')predicates.limitAs(30)constrows=awaitthis.queryRows(predicates)returnrows.map(row=>this.toRecipeCard(row))}}Repository 不需要知道输入框现在是什么。它只负责把某个关键词查出来。能不能回写,是 ViewModel 的职责。
页面层只绑定状态
@Componentexportstruct SearchPage{@StateviewModel:SearchViewModel=newSearchViewModel()build(){Column(){SearchInput({value:this.viewModel.keyword,onChange:(value:string)=>this.viewModel.search(value),onClear:()=>this.viewModel.clearKeyword()})SearchResultList({loading:this.viewModel.loading,errorText:this.viewModel.errorText,recipes:this.viewModel.result})}}}页面层不要再自己判断旧请求。它只要把输入动作交给 ViewModel,然后根据 ViewModel 状态渲染。
防抖可以加,但不是唯一防线
exportclassSearchDebouncer{privatetimer:number=0run(task:()=>void,delay:number=250):void{if(this.timer>0){clearTimeout(this.timer)}this.timer=setTimeout(task,delay)}}防抖的作用是少发请求,requestVersion的作用是防止旧请求回写。两个解决的是不同问题,不要把它们混成一个。
回归验收记录
| 步骤 | 操作 | 预期 |
|---|---|---|
| 1 | 快速输入“番”“番茄”“番茄炒蛋” | 最终只显示最后一个关键词结果 |
| 2 | 人为延迟第一个请求 | 旧请求返回后不能覆盖新列表 |
| 3 | 清空输入框 | 回到默认推荐,不保留旧搜索结果 |
| 4 | 搜索失败 | 保留关键词和错误态,不乱写空结果 |
| 5 | 切换分类再搜索 | requestVersion 重新生效 |
| 6 | 返回搜索页 | 输入框和结果保持一致 |
排查顺序
| 顺序 | 先看什么 | 为什么先看它 |
|---|---|---|
| 1 | 请求返回前有没有版本判断 | 没有判断就会被旧请求覆盖 |
| 2 | 清空输入是否递增版本 | 清空也可能被旧请求回写 |
| 3 | loading 是否按版本关闭 | 旧请求不能把新请求的 loading 关掉 |
| 4 | 防抖和版本号是否同时存在 | 防抖减少请求,版本号保证正确 |
边界责任
| 层级 | 应该负责什么 | 不应该负责什么 |
|---|---|---|
| SearchPage | 绑定输入框和结果列表 | 判断哪个请求有效 |
| SearchViewModel | 维护 requestVersion 和页面状态 | 拼复杂 SQL |
| Repository | 根据关键词查 RDB | 决定是否回写页面 |
| Debouncer | 减少输入触发频率 | 替代请求版本判断 |
小结
搜索页最怕的是“输入框和结果不一致”。用户看到这种错乱,会直接怀疑搜索功能不可信。
中式美食这里我会把时序控制放在 ViewModel:输入变化递增版本,请求返回前比较版本,清空输入也递增版本。Repository 只查数据,页面只渲染状态。这样即使网络慢、用户输入快,旧请求也不能把新结果覆盖掉。
