横向滑动子元素时锁定父元素垂直滚动的问题修复
解决React横向滑动Hook的垂直拖拽与触摸事件取消错误
问题场景
开发React Hook监听元素横向滑动,需求是当横向滑动偏移超过5px时锁定父元素垂直滚动,避免横向滑动时出现垂直拖拽。但实现后控制台触发以下错误:[Intervention] Ignored attempt to cancel a touchmove event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.
尝试检查e.cancelable为真才执行逻辑后,Hook直接失效。
错误根源
- 滚动触发后无法取消事件:浏览器已启动垂直滚动时,touchmove事件的
cancelable属性变为false,此时调用preventDefault()会触发警告。 - 重复绑定事件监听器:原代码每次
touchmove都会给父容器添加一次cancelTouch监听器,导致多次执行preventDefault。 - DOM元素获取方式不合理:直接用
document.getElementById可能在组件挂载前获取不到元素,不符合React数据流模式。 - touchAction设置逻辑冲突:动态切换
touchAction的同时叠加事件监听,导致浏览器触摸行为处理混乱。
解决方案
- 用React Ref跟踪锁定状态,避免重复绑定事件
- 在取消触摸事件前先检查
e.cancelable,仅允许取消可中断的事件 - 用
touchAction: pan-x替代none,精准控制允许的触摸方向 - 通过React Ref传递父容器引用,替代直接操作DOM
- 对比横向/纵向偏移量,仅当用户明确横向滑动时才锁定垂直滚动
修改后的完整代码
Hook实现(TypeScript版)
import { useRef } from "react"; export const useSwipe = (containerRef: React.RefObject<HTMLElement>) => { const touchStartX = useRef<number | null>(null); const touchStartY = useRef<number | null>(null); const isHorizontalLocked = useRef(false); const minSwipeDistance = 100; const lockThreshold = 5; const cancelTouch = (e: TouchEvent) => { // 仅取消可中断的事件,避免警告 if (e.cancelable) { e.preventDefault(); } }; const onTouchStart = (e: React.TouchEvent) => { touchStartX.current = e.targetTouches[0].clientX; touchStartY.current = e.targetTouches[0].clientY; isHorizontalLocked.current = false; }; const onTouchMove = (e: React.TouchEvent) => { if (!touchStartX.current || !touchStartY.current) return; const currentX = e.targetTouches[0].clientX; const currentY = e.targetTouches[0].clientY; const deltaX = Math.abs(currentX - touchStartX.current); const deltaY = Math.abs(currentY - touchStartY.current); // 仅首次检测到横向滑动超过阈值时锁定 if (deltaX > lockThreshold && deltaX > deltaY && !isHorizontalLocked.current) { isHorizontalLocked.current = true; const box = containerRef.current; if (box) { box.style.touchAction = "pan-x"; // 仅允许横向滑动 box.addEventListener("touchmove", cancelTouch, { passive: false }); } } }; const onTouchEnd = () => { const box = containerRef.current; if (box) { box.style.touchAction = "auto"; box.removeEventListener("touchmove", cancelTouch); } if (!touchStartX.current) return; const currentX = touchEndX?.current || touchStartX.current; const distanceX = touchStartX.current - currentX; const isLeftSwipe = distanceX > minSwipeDistance; const isRightSwipe = distanceX < -minSwipeDistance; isHorizontalLocked.current = false; return isRightSwipe ? "right" : isLeftSwipe ? "left" : undefined; }; return { onTouchStart, onTouchMove, onTouchEnd }; };
组件使用示例(JavaScript版)
const { useRef } = React; const useSwipe = (containerRef) => { const touchStartX = useRef(null); const touchStartY = useRef(null); const isHorizontalLocked = useRef(false); const minSwipeDistance = 100; const lockThreshold = 5; const cancelTouch = (e) => { if (e.cancelable) { e.preventDefault(); } }; const onTouchStart = (e) => { touchStartX.current = e.targetTouches[0].clientX; touchStartY.current = e.targetTouches[0].clientY; isHorizontalLocked.current = false; }; const onTouchMove = (e) => { if (!touchStartX.current || !touchStartY.current) return; const currentX = e.targetTouches[0].clientX; const currentY = e.targetTouches[0].clientY; const deltaX = Math.abs(currentX - touchStartX.current); const deltaY = Math.abs(currentY - touchStartY.current); if (deltaX > lockThreshold && deltaX > deltaY && !isHorizontalLocked.current) { isHorizontalLocked.current = true; const box = containerRef.current; if (box) { box.style.touchAction = "pan-x"; box.addEventListener("touchmove", cancelTouch, { passive: false }); } } }; const onTouchEnd = () => { const box = containerRef.current; if (box) { box.style.touchAction = "auto"; box.removeEventListener("touchmove", cancelTouch); } if (!touchStartX.current) return; const currentX = touchEndX?.current || touchStartX.current; const distanceX = touchStartX.current - currentX; const isLeftSwipe = distanceX > minSwipeDistance; const isRightSwipe = distanceX < -minSwipeDistance; isHorizontalLocked.current = false; return isRightSwipe ? "right" : isLeftSwipe ? "left" : undefined; }; return { onTouchStart, onTouchMove, onTouchEnd }; }; const List = () => { const containerRef = useRef(null); const { onTouchStart, onTouchMove, onTouchEnd } = useSwipe(containerRef); const handleTouchEnd = () => { const swipeDirection = onTouchEnd(); if (swipeDirection === "left") console.log("左滑"); if (swipeDirection === "right") console.log("右滑"); }; const items = Array(30).fill(""); return ( <div ref={containerRef} className="container" id="scrollable-cards-box"> {items.map((_, index) => ( <ul className="item" key={index} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={handleTouchEnd} > 列表项 {index + 1} </ul> ))} </div> ); }; ReactDOM.createRoot(document.getElementById("root")).render(<List />);
样式代码
.container { background-color: gray; overflow-y: auto; height: 400px; } .item { background-color: skyblue; padding: 15px 5px; margin: 5px 0; list-style: none; }
HTML结构
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script> <div id="root"></div>
关键改进说明
- 状态跟踪:用
isHorizontalLocked避免重复绑定事件和样式设置,提升性能 - 事件安全处理:检查
e.cancelable彻底消除控制台警告 - 精准触摸控制:
touchAction: pan-x明确允许横向滑动,同时禁止垂直滚动 - React最佳实践:通过Ref传递容器引用,避免直接操作DOM
- 滑动判断优化:对比横向/纵向偏移量,确保锁定逻辑仅在用户明确横向滑动时触发
内容的提问来源于stack exchange,提问作者magrega
相关产品推荐
相关产品推荐

