为什么 touchstart 事件在 iOS 上有时不触发?
我在做一个移动端的滑动组件,用了 touchstart 监听手指按下,但在 iPhone 上经常点好几次才触发一次,安卓却正常。
试过给元素加 cursor: pointer 和 touch-action: manipulation,也确保没被其他事件冒泡干扰,但问题还在。是不是 iOS 对 touch 事件有啥特殊限制?
element.addEventListener('touchstart', (e) => {
console.log('touch start!');
e.preventDefault();
});
常见的解决方案主要有这几个方向:
第一,passive 参数问题。iOS Safari 从某个版本开始,touchstart 和 touchmove 默认是 passive 模式,你在 passive 监听器里调用 preventDefault() 会直接被忽略。改成这样:
第二,检查 CSS 里有没有 user-select: none。iOS 上如果元素或父元素有这个属性,touchstart 会间歇性失效,系统会把你的滑动当成选择文本的操作处理。要么删掉这个属性,要么加上 -webkit-touch-callout: none 和 -webkit-user-select: none 配合使用。
第三,元素本身要"可点击"。iOS 判断元素是否响应 touch 事件有个内部逻辑,纯 div、span 这种有时候会被忽略。你加的 cursor: pointer 是对的,但还要确认元素有实际的宽高,别是 display: none 或者 visibility: hidden 状态下绑定的事件。
第四,fastclick 冲突。如果你项目里引入了 fastclick 库或者类似的处理 300ms 延迟的方案,可能会和原生 touch 事件打架,点几次才触发通常是这个原因。试试在 problematic 元素上加 needsclick 类名,或者干脆把 fastclick 干掉,现在大部分浏览器已经不需要这玩意了。
最可能的就是第一个,passive 参数没加。改完基本就好了。
先说现象:你点很多次才触发一次,不是因为 touchstart 不工作,而是 Safari 在判断你到底是不是想「点击」——它会等待一小段时间(大概 300ms),看后续有没有 click 事件冒泡上来,如果判定你是「快速双击」或者「缩放意图」,就可能跳过 touchstart 或延迟触发。
更关键的是:你代码里调用了
e.preventDefault(),但没在 touchstart 里调用,而是在 touchmove 或 touchend 里调用,会导致 Safari 把这次交互标记为「可能触发滚动」,从而主动抑制后续 touch 事件,尤其是当元素本身没有可滚动内容时——它会认为你没明确表示「不希望滚动」,所以干脆不给你 touchstart。解决方案分三步:
第一步:确保在
touchstart事件里就调用e.preventDefault(),而且只在你需要禁止默认行为的时候才调用(比如做自定义滑动时)。如果你只是监听按下,不需要阻止滚动,就别乱调preventDefault,否则 Safari 会「宁可错杀一千」。第二步:给目标元素加
touch-action: none;(注意不是manipulation,虽然manipulation是好习惯,但对自定义手势组件来说不够)。touch-action: none;会明确告诉浏览器「这个元素内部所有触摸操作都由我自定义处理」,Safari 就不会再犹豫要不要触发 touch 事件了。第三步:如果你是做滑动组件(比如轮播图、侧滑菜单),建议同时监听
touchstart、touchmove、touchend,并在touchstart里记录起始坐标,在touchmove里根据滑动方向决定是否调用preventDefault,这样既保证响应及时,又避免误阻止滚动。举个实际能用的代码例子(带注释):
顺便说一句,
passive: false必须显式加,因为现代浏览器默认把 touch 事件设为 passive(为了性能),一旦 passive 是 true,你在回调里调preventDefault()就会被忽略,Safari 还可能因此触发「降级策略」——比如延迟触发事件。还有一个隐藏坑:如果你用的是
document或window监听 touchstart,而不是具体元素,iOS 会更「谨慎」,因为它要判断点击是否落在可交互元素上(比如<div>默认不可点击,而<button>可以)。所以尽量绑定到具体元素上,或者给元素加个role="button"或tabindex="0"(虽然不是必须,但能减少 Safari 的猜测)。最后说个真实经验:iPhone 12 之后的机型(iOS 14.5+)已经基本解决了 300ms 延迟,但老旧机型(比如 iPhone 6s、iPhone SE 第一代)还是有这个问题,所以如果你测试只在新机上没问题,老机上抽风,那大概率是 passive 和 preventDefault 的时机问题,不是你的代码逻辑错。
赶紧试试把
passive: false加上,再在touchstart里直接preventDefault,大概率就治好了。