为什么 touchstart 事件在 iOS 上有时不触发?

夏侯世梅 阅读 21

我在做一个移动端的滑动组件,用了 touchstart 监听手指按下,但在 iPhone 上经常点好几次才触发一次,安卓却正常。

试过给元素加 cursor: pointertouch-action: manipulation,也确保没被其他事件冒泡干扰,但问题还在。是不是 iOS 对 touch 事件有啥特殊限制?

element.addEventListener('touchstart', (e) => {
  console.log('touch start!');
  e.preventDefault();
});
我来解答 赞 1 收藏
二维码
手机扫码查看
2 条解答
美玲~
美玲~ Lv1
这个问题我之前也踩过坑,iOS 的 touch 事件确实有几个比较坑的地方。

常见的解决方案主要有这几个方向:

第一,passive 参数问题。iOS Safari 从某个版本开始,touchstart 和 touchmove 默认是 passive 模式,你在 passive 监听器里调用 preventDefault() 会直接被忽略。改成这样:

element.addEventListener('touchstart', (e) => {
console.log('touch start!');
e.preventDefault();
}, { passive: false });


第二,检查 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 参数没加。改完基本就好了。
点赞 1
2026-03-02 05:02
夏侯莉莉
这个问题我之前也踩过坑,而且不是你一个人遇到的,根本原因不在 touchstart 事件本身,而在 iOS 的「300ms 点击延迟」和「点击穿透」机制,以及 Safari 对 touch 事件触发的「保守策略」。

先说现象:你点很多次才触发一次,不是因为 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 事件了。

第三步:如果你是做滑动组件(比如轮播图、侧滑菜单),建议同时监听 touchstarttouchmovetouchend,并在 touchstart 里记录起始坐标,在 touchmove 里根据滑动方向决定是否调用 preventDefault,这样既保证响应及时,又避免误阻止滚动。

举个实际能用的代码例子(带注释):

let startX = 0;
let startY = 0;
let isMoving = false;

element.addEventListener('touchstart', (e) => {
// 只有在第一次触摸时记录坐标
if (e.touches.length === 1) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
isMoving = false;
console.log('touch start!');
// ✅ 关键:如果确定不需要滚动,直接 preventDefault
// 不要等到 touchmove 里才调,那样可能已经晚了
e.preventDefault();
}
}, { passive: false }); // 注意这里必须显式设 passive: false,否则 preventDefault 不生效

element.addEventListener('touchmove', (e) => {
if (e.touches.length !== 1) return;
const deltaX = e.touches[0].clientX - startX;
const deltaY = e.touches[0].clientY - startY;

// 比如:只在水平滑动超过 10px 时才阻止默认滚动
if (Math.abs(deltaX) > 10 && Math.abs(deltaX) > Math.abs(deltaY)) {
isMoving = true;
e.preventDefault(); // ✅ 这里再 preventDefault 也来得及
}
}, { passive: false });

element.addEventListener('touchend', (e) => {
if (!isMoving && Math.abs(e.changedTouches[0].clientX - startX) < 5) {
console.log('实际是点击,不是滑动');
}
});


顺便说一句,passive: false 必须显式加,因为现代浏览器默认把 touch 事件设为 passive(为了性能),一旦 passive 是 true,你在回调里调 preventDefault() 就会被忽略,Safari 还可能因此触发「降级策略」——比如延迟触发事件。

还有一个隐藏坑:如果你用的是 documentwindow 监听 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,大概率就治好了。
点赞 2
2026-02-27 11:20