<p>window.onscroll 直接监听易卡顿,因滚动事件触发频率极高,不做节流会堆积大量请求;且滚动到底部判断若用 document.documentElement.scrollHeight - window.innerHeight ≤ window.scrollY,会因精度误差或布局抖动导致重复触发。</p>

为什么 window.onscroll 直接监听容易卡顿甚至重复请求
上拉加载本质是滚动事件触发数据拉取,但 window.onscroll 触发频率极高(每像素都可能触发),不做节流会瞬间堆积大量请求或重绘。更关键的是:滚动到底部的判断逻辑若写成 document.documentElement.scrollHeight - window.innerHeight ,在 iOS Safari 或某些安卓 WebView 中因渲染延迟/缩放/滚动惯性,常误判为“已到底”,导致多次触发同一页。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 用
IntersectionObserver替代onscroll监听底部占位元素(如<div id="loader-ref"></div>),天然防抖、兼容性好(iOS 12.2+、Chrome 51+) - 若必须用 scroll,至少加
requestIdleCallback或setTimeout(..., 0)延迟检查,且每次检查前设loading = true开关,请求返回后才置false - 底部判定改用
el.getBoundingClientRect().top (<code>el是 loader-ref 元素),绕过文档高度计算误差
fetch 请求并发控制与 loading 状态管理怎么不乱
用户快速上拉时,可能连续触发 2–3 次加载,但后发请求本应被丢弃(因新数据已覆盖旧状态),结果却把旧页数据塞进列表末尾,造成错序或重复。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 每个请求绑定唯一
AbortController,新请求发起前调用上一个abort(),避免“幽灵响应”更新 DOM - 维护一个
pendingRequest变量,值为当前请求的page或cursor;新请求只在pendingRequest 时才发出(适用于 cursor 分页) - DOM 更新前校验:插入新数据前,先确认
data.cursor === lastLoadedCursor + 1(或页码严格递增),否则跳过
如何让上拉加载不破坏浏览器原生滚动位置和 history
SPA 场景下,用户从详情页返回列表页,若用上拉加载动态拼接内容,history.state 未记录当前已加载页数,会导致返回后滚动位置丢失、首屏空白、甚至重新加载第 1 页。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 每次成功加载一页后,调用
history.replaceState({ page: next }, "", ""),把当前页码存入 state - 页面初始化时,读取
history.state?.page,优先用它决定起始加载位置,而非硬编码page = 1 - 监听
popstate事件,当用户前进/后退时,根据event.state?.page跳转到对应位置(可用el.scrollIntoView({ block: "start" })定位到该页第一条数据)
服务端 cursor 分页比 offset 分页更适合上拉加载
前端传 offset=20 请求第 2 页,但服务端数据实时增删时,offset 会跳过或重复记录(尤其高并发写入场景)。而 cursor(如最后一条的 id 或 created_at)基于数据快照,稳定可预测。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 后端接口统一返回
{ data: [...], next_cursor: "12345" },前端仅需把上一次的next_cursor当作下一次请求参数 - 前端缓存所有已加载的
cursor链,防止用户反复上下拉时反复请求同一段 - 首次加载不带
cursor,后续请求必须带;服务端对缺失cursor返回 400,前端不静默 fallback
真正难的不是“拉下来”,而是拉下来之后数据怎么不打架、滚动怎么不跳、返回怎么不懵——这些细节藏在 AbortController 的调用时机、IntersectionObserver 的 rootMargin 设置、以及每次 replaceState 的 key 命名里。



















