根本原因是list属性值与datalist的id未严格匹配(大小写、空格等差异),且datalist必须在DOM加载时存在、仅value属性参与前缀匹配,移动端兼容性差。

为什么加了 list 属性却没下拉?
根本不是代码写错了,而是浏览器根本不“认”这个绑定。最常见的是 list 属性值和 <datalist id=""></datalist> 的 id 不完全一致——大小写、空格、中文、连字符差异都会导致失效。比如 list="cities" 对应的必须是 <datalist id="cities"></datalist>,不能是 id="Cities" 或 id=" cities "。
另外两个隐形杀手:
-
<datalist></datalist>必须在 HTML 加载时就存在于 DOM 中,用 JS 动态插入后,部分浏览器(尤其是 Safari)压根不识别 -
<option></option>只读取value属性做匹配,textContent或label不参与筛选,写了也白写
Firefox 还有个怪癖:得先 focus 输入框,再按 ↓ 键才弹出选项;Chrome 和 Edge 相对友好,但依然只支持前缀匹配——输 “北” 才出 “北京”,输 “京” 就什么也没有。
input 事件监听 vs keyup,选哪个?
选 input。它能捕获粘贴、拖入、语音输入、IME 上屏等所有值变更场景;keyup 漏掉粘贴,也抓不到中文输入法组合完成前的中间态。
立即学习“前端免费学习笔记(深入)”;
但中文输入法有坑:用户敲 “shang” 还没按空格上屏时,input.value 还是旧值,立刻查词就会错。必须配合 compositionstart 和 compositionend:
- 监听到
compositionstart时暂停联想请求 - 等
compositionend触发后再执行过滤或 fetch - Safari 移动端对 composition 事件支持弱,可 fallback 到
setTimeout(() => {...}, 100)延迟查词
动态联想怎么防抖 + 避免请求打架?
不加防抖,用户敲 “react” 会发出 5 次请求;不处理并发,后发的请求慢,返回后直接覆盖掉先发的正确结果——这是真实线上 bug 的高频来源。
基础防抖写法(不用 Lodash):
let debounceTimer;
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
const value = input.value.trim();
if (value.length < 2) return;
debounceTimer = setTimeout(() => fetchSuggestions(value), 300);
});
更稳妥的做法是搭配 AbortController:
- 每次新请求前调用
abortController.abort() - fetch 时传入
{ signal: abortController.signal } - 避免旧请求响应回来后错误地更新 UI
键盘导航和焦点管理最容易漏什么?
不是没写 ArrowDown/ArrowUp,而是状态不同步。比如用户用键盘上下移动高亮项,鼠标一 hover 就把高亮切走;或者点了某条建议,列表隐藏了,但输入框 focus 状态丢了,导致回车没反应。
关键点只有三个:
- 维护一个
activeIndex变量,仅由键盘操作修改,鼠标 hover 不改它 - 点击建议项后,除了填值,必须手动触发
input.dispatchEvent(new Event('input', { bubbles: true })),否则 Vue/React 绑定不会更新 -
blur或按Escape时,清空列表同时确保activeIndex = -1,防止下次打开时残留高亮
移动端软键盘弹起时,getBoundingClientRect() 比 window.innerHeight 可靠得多——后者在 iOS 上常因视口缩放失真,导致下拉框飘到屏幕外。



















