用Iterator和生成器实现数据库流式分页,核心是按需拉取、边查边吐,后端采用基于排序字段的游标分页(如WHERE id > ?),前端用async generator封装请求并yield每页数据,支持for await...of消费,可结合AbortSignal控制生命周期。

用 Iterator 和生成器实现数据库流式分页,核心是“按需拉取、边查边吐”,不一次性加载全部数据,而是每次只取一页所需记录,并保持查询上下文(比如游标或 last_id)。关键不在前端 JS 处理数据,而在于与后端配合设计可中断、可续的分页接口,前端用生成器封装成同步风格的迭代逻辑。
后端需提供游标分页接口
传统 offset/limit 在大数据量下性能差且无法保证顺序一致性。应改用基于排序字段(如 created_at 或 id)的游标分页:
- 请求时传入
cursor(例如上一页最后一条记录的id或编码后的时间戳) - 响应返回当前页数据 + 下一页
next_cursor(非 null 表示还有更多) - SQL 示例:
SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 20
用 async generator 封装分页请求
前端用 async function* 按需发起请求,每次 yield 一页数据,调用方可用 for await...of 流式消费:
async function* fetchPageStream(url, pageSize = 20) {
let cursor = null;
while (true) {
const params = new URLSearchParams({ limit: pageSize });
if (cursor !== null) params.set('cursor', cursor);
<pre class="brush:php;toolbar:false;">const res = await fetch(`${url}?${params}`);
const { data, next_cursor } = await res.json();
yield data; // 一次 yield 一整页数组
if (!next_cursor) break;
cursor = next_cursor;} }
使用方式:
立即学习“Java免费学习笔记(深入)”;
for await (const page of fetchPageStream('/api/logs')) {
for (const item of page) {
renderLog(item); // 边拿到边渲染,不等全部加载完
}
}用 Iterable + Symbol.iterator 做同步风格适配(可选)
如果某些场景必须同步迭代(如配合某些老库),可用包装类模拟同步 Iterator,但底层仍是异步拉取 + 缓存机制:
- 内部维护一个 pending 队列和已缓存页
-
next()调用时,若缓存为空则 await 下一页并缓存 - 注意:这不是真正同步,只是语法伪装,仍需在 async 函数中使用
示例简版:
class CursorPaginatedList {
constructor(url, pageSize = 20) {
this.url = url;
this.pageSize = pageSize;
this.cursor = null;
this.cache = [];
this.done = false;
}
<p>async next() {
if (this.cache.length === 0 && !this.done) {
const params = new URLSearchParams({ limit: this.pageSize });
if (this.cursor) params.set('cursor', this.cursor);</p><pre class="brush:php;toolbar:false;"> const res = await fetch(`${this.url}?${params}`);
const { data, next_cursor } = await res.json();
this.cache = data;
this.cursor = next_cursor;
this.done = !next_cursor;
}
return { value: this.cache.shift(), done: this.cache.length === 0 && this.done };}
[Symbol.iterator]() { return this; } }
结合 AbortSignal 控制流生命周期
用户中途取消滚动或切换页面时,应中止后续请求:
- 在
fetch中传入{ signal } - 每次新请求前调用
abortController.abort(),再新建控制器 - 生成器内部需捕获
AbortError并 clean up
这样既节省带宽,也避免内存堆积旧页数据。


















