根本原因是滚动容器高度未固定或内容动态变化后未重置滚动位置,导致 scroll-view 误判触底;需设显式 height、保证列表项高度确定、加载后主动调整 scrollTop 并加 loading 守卫。

为什么 scroll-view 的 @scrolltolower 会反复触发?
根本原因是滚动容器高度未固定,或内容动态变化后未重置滚动位置,导致 scroll-view 在内容追加后“误判”自己仍处于底部。常见于:列表项高度不一致(如图文混排)、图片懒加载完成前占位高度为 0、字体加载延迟造成布局重排。
更隐蔽的情况是:在 @scrolltolower 回调里直接 this.list.push(...) 后,新内容撑高了 scroll-view 内部高度,但 scroll-view 自身 scrollTop 未归零,下次滚动稍一松手就又满足“触底”条件。
- 必须给
设置显式height或max-height(不能依赖 flex 布局自动撑开) - 所有列表项需有确定高度,避免图片用
width/height但没设aspect-ratio或占位骨架 - 在
@scrolltolower触发后,立即设this.loading = true,并在请求开始前加守卫:if (this.loading || this.noMore) return
onReachBottom 和 scroll-view 混用时的重复加载陷阱
页面同时存在 onReachBottom 和内部 时,极易出现双触发——用户滑到底部,onReachBottom 先执行一次,紧接着 的 @scrolltolower 又执行一次。这不是 bug,是滚动域隔离失效的典型表现。
uni-app 不允许两个滚动源共存于同一交互路径。一旦用了 ,就必须彻底放弃 onReachBottom;反之,若依赖页面级触底,则整个页面内容必须可自然滚动(即不能包裹在 fixed 高度的 scroll-view 里)。
- 检查
pages.json中该页面是否误配了onReachBottomDistance,删掉它 - 确认
外层没有多余position: fixed或height: 100vh的父容器 - 真机调试时,在两个回调里都加
console.log('onReachBottom')和console.log('scrolltolower'),看是否同时输出
数据追加后如何防止滚动位置“假触底”?
每次追加数据后,scroll-view 的实际滚动高度变了,但它的 scrollTop 值仍停留在旧位置。如果新内容刚好让当前可视区域底部对齐 scroll-view 底部,就会立刻再次触发 @scrolltolower。
这不是防抖能解决的问题——它是滚动容器状态和 DOM 高度不同步导致的必然结果。必须主动干预滚动位置。
- 在
loadMore成功回调末尾,手动调用this.$nextTick(() => { uni.createSelectorQuery().select('.my-scroll').boundingClientRect(res => { if (res) uni.createScrollSelectorQuery().scrollTo({ selector: '.my-scroll', scrollTop: res.height }) }).exec() })(仅 H5/App 支持) - 更通用做法:加载完成后,设置一个短暂延时(
setTimeout(() => { this.loading = false }, 100)),让视图更新完成后再开放下一次触发 - 关键判断点:只有当
newData.length > 0且this.list.length >= this.total时,才设this.noMore = true;否则永远保留加载入口
uni-app x / UTS 场景下的特殊处理
在 uni-app x(尤其是使用 UTS 编写的 nvue 页面)中,@scrolltolower 的触发精度更高,但也更敏感。UTS 运行时对滚动事件的采样频率远高于 JS 层,轻微抖动就可能上报多次。
此时靠 Vue 层防抖已不够,必须下沉到原生事件过滤层。UTS 提供了 scrollThreshold 参数,可设为 20(单位 px)来抑制微小滚动抖动。
- nvue 页面中,
必须显式写scroll-y="true",且禁用enable-back-to-top(它会干扰触底判断) - UTS 代码中监听
scrollend事件比scrolltolower更可靠,因前者明确表示滚动结束,后者只表示“接近底部” - 不要在 UTS 里做数组合并:
this.list = [...this.list, ...data],而要用this.list.push(...data)并调用this.$forceUpdate()强制刷新
真正难的不是写对第一次加载,而是让第 5 次、第 12 次加载都稳定触发且不重叠——这取决于你是否在每次数据变更后,同步修正了滚动容器的高度认知和位置状态。











