为什么移动端touch事件在子元素滑动时触发了父元素的touchmove?
我在开发移动端侧滑菜单时遇到问题:父容器有touchmove监听处理全局滚动,但子元素的可滑动区域(比如卡片)滑动时,父元素的事件也跟着触发了。我试过在子元素的touchmove里调用stopPropagation(),但父元素的事件还是会被触发,导致滑动卡顿。
代码大概是这样的:
document.querySelector('.parent').addEventListener('touchmove', (e) => {
// 处理页面滚动逻辑
e.preventDefault();
});
document.querySelector('.child').addEventListener('touchmove', (e) => {
// 处理卡片滑动逻辑
e.stopPropagation(); // 这里没生效?
});
后来发现如果把父元素的preventDefault()去掉,子元素滑动正常了,但全局滚动又失效了。有没有办法让两者同时生效?是不是事件委托的问题?
你父元素加了 e.preventDefault(),这会导致整个页面的默认触摸行为被立即阻止——包括子元素的滚动。关键是:preventDefault 一旦在 touchstart 或 touchmove 早期触发,浏览器就会锁定所有滚动能力,后续不管你怎么 stopPropagation 都救不回来。
stopPropagation 确实能阻止事件冒泡,但前提是事件能先在子元素被捕获。而真正的坑在于:移动端触摸行为是同步判定的,浏览器在 touchstart 阶段就要决定“这个 touch 是用来滚动的还是交给 JS 处理的”。如果你父级无条件 preventDefault,那浏览器直接认定“用户想自定义操作”,于是禁用所有原生滚动,连带子元素的滑动也废了。
正确做法是:只在真正需要全局滚动时才调用 preventDefault。你可以通过判断滑动方向和当前元素是否可滚动来决策。
参考这样改:
关键点:
- 给父元素 touchmove 加
{ passive: false },否则 preventDefault 被忽略(现代浏览器默认 passive 为 true)- 不要无脑 preventDefault,要根据目标元素的可滚动状态动态决定
- 子元素正常滑动时,让浏览器保留默认滚动行为
这样既能保证子元素滑动流畅,又能在需要时触发父级逻辑。说白了就是别一上来就劫持所有触摸,得跟浏览器“协商”着来。
stopPropagation()没用,是因为preventDefault()和事件冒泡是两个不同阶段的操作,而且touchmove是持续触发的,父子元素的事件处理很容易互相干扰。解决办法是:**在子元素滑动时,不让父元素的
touchmove触发滚动逻辑**。你可以用一个标志位控制。改法如下:
这样处理之后,子元素滑动时会阻止父级滚动逻辑触发,同时父级的滚动在其它区域依然生效。比单纯靠
stopPropagation控制更可靠。另外,如果父级滚动是页面滚动(body滚动),你也可以考虑把
e.preventDefault()放在更合适的时机,比如判断滑动方向或滑动目标区域。不然很容易卡顿或冲突。