微信小程序搜索框删空不刷新:「负向事件」被遗漏的坑
做列表页搜索时,开发者通常只关心「用户输入了什么」,却忽略了另一面——「用户删掉了什么」。这个坑在小程序里几乎是必踩的,复现路径极短,但根因定位却容易绕弯。
现象
复现步骤:
- 打开带搜索框的列表页,输入关键词 →
bind:input触发 → 发请求 → 返回过滤结果 - 长按全选删除 / 连按 backspace 清空输入框 → 输入框为空
- 列表仍然显示旧的过滤结果,没有重拉全量数据
- 点击 tab 切换排序 → 发了新请求,但 keyword 参数时序错乱,或携带残留空白字符 → 仍然只显示过滤子集
用户感知:「我明明删掉了搜索词,为什么还在过滤?」
根因
只处理了正向事件
典型的反模式写法:
// ❌ 反模式
onSearchInput(e) {
this.setData({ keyword: e.detail.value }); // 只改 data,不发请求
},
onSearchConfirm() {
this.getList(true); // 只在用户按「搜索」确认键时才请求
},开发者的心理模型是「输入过程中别频繁请求,只在用户确认时发一次」——这是对的,但漏掉了一个负向事件:
从
"关键词" → "",在用户心智里等于「我取消这个筛选条件了」,应该等价于点 X 清空。
bind:input 只把 data.keyword 改成了空字符串,列表里的过滤结果没被清——UI 状态和数据状态永久错位。
bind:input 只负责把输入值同步到 data,它不会自动触发 getList。当 keyword 从有值变成空字符串时,列表停在了上一次请求的结果里,而用户看到的空搜索框根本不知道还有「幽灵过滤」在生效。
为什么切 tab 也没救
changeSort / changeTab 里调用 getList,逻辑上看应该能刷——但如果用户没删干净(末尾留了一个空格),keyword 是 " " 而非 "",请求参数就会带上这个「伪非空」值,后端仍然执行 LIKE '% %' 导致莫名过滤。更糟糕的是,setData 是异步的,如果 changeSort 先读了 this.data.keyword,可能拿到前一帧的老值,产生时序竞争。
正解
在 onInput 里检测「删空」并立即触发
// ✅ 修复方案
onSearchInput(e) {
const newVal = e.detail.value;
const prevVal = this.data.keyword;
this.setData({ keyword: newVal });
// 负向事件:从有值变为空(含空白字符) → 等价于点 X 清空
if (prevVal && !newVal.trim()) {
this.getList(true);
}
},
onSearchConfirm() {
this.getList(true);
},使用 van-search 时,同步绑定 bind:clear
Vant Weapp 的 van-search 右侧 X 图标不走 bind:change,走的是独立的 bind:clear 事件:
<van-search
value="{{ keyword }}"
placeholder="搜索"
bind:change="onSearchInput"
bind:search="onSearchConfirm"
bind:clear="onSearchClear"
/>onSearchClear() {
this.setData({ keyword: '' });
this.getList(true);
},双保险:用户点 X 走 bind:clear,backspace 删空走 bind:change + 空值检测,两条路都覆盖。
onInput 里判断 prevVal && !newVal.trim(),检测到「从非空变空」时立即 getList(true)。同时绑定 bind:clear 处理 X 图标点击。请求参数组装时也要对 keyword 做 trim(),防止空格伪非空绕过过滤。
请求侧也要 trim
getList(reset) {
const keyword = (this.data.keyword || '').trim(); // 必须 trim
wx.request({ data: { keyword, page: this.data.page, ... } });
}可选:与防抖结合
如果要做「边输边搜」,防抖和清空检测不冲突——清空走立即触发,其余走 debounce:
onSearchInput(e) {
const newVal = e.detail.value;
const prevVal = this.data.keyword;
this.setData({ keyword: newVal });
if (prevVal && !newVal.trim()) {
this.getList(true); // 清空立即触发,不走防抖
return;
}
clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => this.getList(true), 400);
},一句话外卖
搜索框的「清空」是一个独立的负向事件,必须和「确认搜索」享有同等的请求触发权重——凡是能改变过滤条件的用户行为,都要让列表状态即时收敛。