直接给body绑定touchmove会废掉页面滚动,根本原因是无条件调用preventdefault()导致浏览器禁用原生滚动引擎;正确做法是仅在scrolltop=0且scrollheight-clientheight

为什么直接给 body 绑定 touchmove 会废掉页面滚动
根本原因是:一旦在 touchmove 中无条件调用 event.preventDefault(),浏览器就认为你“接管了滚动”,原生滚动引擎被禁用,整个页面失去上下滑动能力。这不是 bug,是规范行为。
真正要做的,是只在「用户确实在顶部、且手指正向下拖」时才干预:
- 记录
touchstart时的element.scrollTop和touches[0].clientY -
touchmove中仅当element.scrollTop 0(注意用,兼容 iOS overscroll 负值)才更新下拉偏移并调用 <code>preventDefault() - 其他情况(比如已滚动一段距离、或手指上滑)完全不干预,让原生滚动继续工作
scrollTop === 0 判断不准?用 getBoundingClientRect() 辅助校验
单纯依赖 scrollTop 在某些布局下会失效:flex 容器 + overflow: auto、iOS Safari 的弹性滚动回弹、甚至某些 CSS transform 场景,都可能导致 scrollTop 暂时为 0 但实际内容并未贴顶。
更稳妥的方式是双校验:
- 主判:
element.scrollTop - 辅判:
element.getBoundingClientRect().top >= 0(容器自身是否可见且顶部未被遮挡) - 再加一层:
element.scrollHeight - element.clientHeight 排除“内容过短、本就不该滚动”的误触发
三者同时满足,才认为处于可下拉刷新的临界位置。
嵌套容器里多个 touch 监听器互相打架怎么办
常见于轮播图(Swiper)、卡片列表、吸顶导航栏共存的页面——每个组件都绑了自己的 touchstart/touchmove,结果手势被某一层提前 consume,其他层收不到事件。
解决思路不是“谁优先”,而是“谁该响应”:
- 给轮播图容器加
touch-action: pan-y,明确告诉浏览器:这个区域只响应纵向滑动,横向 swipe 手势由它自己处理 - 给刷新容器(如
#feed)加overscroll-behavior: contain,防止 iOS 弹性滚动穿透影响判断 - 所有自定义
touch监听器统一用addEventListener('touchmove', handler, { passive: false }),否则preventDefault()会被忽略 - 避免在
document级全局监听;只绑定到具体刷新目标容器,减少误触范围
iOS Safari 下 touchcancel 频发导致刷新中断
这不是代码问题,是系统级拦截:当手指快速下拉靠近状态栏、或触发返回手势区域时,Safari 主动发出 touchcancel,touchend 永远不会触发。
必须把它当作正常结束流程来处理:
- 在
touchcancel回调里,清空所有缓存的起始坐标(startY、startScrollTop) - 重置当前下拉状态(
isPulling = false、pullOffset = 0) - 如果此时已达到刷新阈值但未松手,**不自动触发刷新**——用户没完成“松手”动作,强制刷新违背直觉
真正容易被忽略的是:touchcancel 后若用户立刻再次下拉,touchstart 的坐标可能异常(比如 clientY 跳变),需要在 touchstart 中做一次有效性过滤,丢弃与前次间隔过近或位移突变的事件。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











