PullToRefresh 在移动端列表里为啥不生效?

南宫书圻 阅读 14

我用的是原生 JS 实现的下拉刷新,监听 touch 事件做的,但在 iOS Safari 上完全没反应,安卓部分机型也偶尔失灵。

试过加 touch-action: none,也阻止了默认滚动,但还是不行。这是我的核心逻辑:

let startY = 0;
const refreshEl = document.querySelector('.pull-refresh');
document.addEventListener('touchstart', e => {
  startY = e.touches[0].pageY;
});
document.addEventListener('touchmove', e => {
  const moveY = e.touches[0].pageY;
  const offset = moveY - startY;
  if (offset > 0 && window.scrollY === 0) {
    refreshEl.style.transform = <code>translateY(${offset * 0.3}px)</code>;
  }
});

是不是漏了什么关键点?比如需要配合 overscroll-behavior 或者别的 meta 标签?

我来解答 赞 2 收藏
二维码
手机扫码查看
2 条解答
书生シ嘉木
问题应该出在事件监听和滚动容器的判断上,iOS Safari 对 touchmove 的默认行为处理特别严格,尤其是当页面可滚动时,即使你判断了 window.scrollY === 0,它也可能不触发 touchmove 或中途被拦截。

你目前的写法有几个硬伤:

1. 监听的是 document 上的 touchstarttouchmove,但移动端很多情况下滚动是发生在 body 或某个具体容器上(比如你列表外面包了一层 overflow: auto 的 div),这时候 window.scrollY 可能根本不会变,或者滚动起点根本不在 window 上;
2. 没有显式调用 preventDefault(),iOS Safari 在滚动边界(比如顶部下拉)时会默认触发“橡皮筋”效果,你的 touchmove 可能被系统抢走了;
3. touchmove 里没有做 passive: false 的声明,导致即使你写了 preventDefault() 也可能无效(Chrome 和 Safari 默认把 touch 事件设为 passive);

正确做法是:

- 把监听器绑定在实际滚动的容器上(比如你的列表容器),而不是 document
- 初始化监听时加上 { passive: false }
- 在 touchmove 里,当检测到下拉且处于顶部时,主动 preventDefault() 阻止默认滚动;
- 滚动容器的 touch-action 要设成 none(但注意是绑定事件的那个元素,不是 body);

给你一个改好的简化版参考:

let startY = 0;
const refreshEl = document.querySelector('.pull-refresh');
const scrollContainer = document.querySelector('.scroll-container'); // 你实际滚动的那个元素

scrollContainer.addEventListener('touchstart', e => {
startY = e.touches[0].pageY;
}, { passive: false });

scrollContainer.addEventListener('touchmove', e => {
e.preventDefault(); // 先阻止默认行为,避免橡皮筋干扰
const moveY = e.touches[0].pageY;
const offset = moveY - startY;
const scrollTop = scrollContainer.scrollTop;

if (offset > 0 && scrollTop <= 0) {
refreshEl.style.transform = translateY(${offset * 0.3}px);
}
}, { passive: false });


另外记得给滚动容器加样式:

.scroll-container {
touch-action: none;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}


如果还是不行,大概率是滚动容器没选对,你得确认一下到底是谁在滚动——有时候你以为是 window 滚,其实被某个 fixed 布局或者 position: absolute 的元素遮住了,实际滚动的是内部 div。
点赞 3
2026-02-27 13:07
W″松静
你这个问题我踩过坑,核心问题在于 iOS Safari 对 touch 事件的处理太“聪明”了,它会在滚动容器还没到顶的时候就抢先吃掉 touchmove,导致你根本收不到事件——哪怕你判断了 window.scrollY === 0

关键点有两个:

第一,iOS Safari 下,如果滚动的是 body,那 window.scrollY 并不可靠,尤其当页面内容少于一屏时,它压根不会触发滚动,你监听的 touchmove 会被浏览器自己拦截做“橡皮筋回弹”动画。

第二,你没处理 touchmovepreventDefault(),浏览器默认会滚动页面,但 iOS 上如果滚动区域不是 body(比如你用了固定高度的 overflow: auto 容器),那 window.scrollY 根本不会变,你永远进不了条件判断。

解决方案直接上代码:

let startY = 0;
let isPulling = false;
const refreshEl = document.querySelector('.pull-refresh');
const scrollContainer = document.body; // 或者你自己的滚动容器

document.addEventListener('touchstart', e => {
startY = e.touches[0].pageY;
isPulling = false;
}, { passive: false });

document.addEventListener('touchmove', e => {
const moveY = e.touches[0].pageY;
const offset = moveY - startY;

// 兼容 iOS:用 scrollContainer.scrollTop === 0 更可靠
const atTop = scrollContainer.scrollTop <= 0;

if (offset > 0 && atTop) {
e.preventDefault(); // 这句必须加!否则 iOS 会继续滚动
isPulling = true;
refreshEl.style.transform = translateY(${offset * 0.3}px);
}
}, { passive: false });

document.addEventListener('touchend', e => {
if (isPulling) {
// 触发刷新逻辑
refreshEl.style.transition = 'transform 0.3s ease';
refreshEl.style.transform = 'translateY(0)';
// 这里写你的刷新回调
setTimeout(() => {
refreshEl.style.transition = '';
}, 300);
}
});


另外别忘了在 meta 里加上这个(虽然不是必须,但能减少 iOS 的手势冲突):



最后提醒一句:别用 document.addEventListener('touchmove', ...) 全局监听,如果页面里有其他可滚动容器(比如弹窗里的列表),会把滚动也吃掉——实际项目里建议只在目标容器上加监听,或者加个 flag 控制。
点赞 1
2026-02-24 12:05