登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

IndexedDB 游标方向怎样影响遍历顺序

来源:17golang原创

时间:2026-11-15 01:03:52 253浏览 收藏

我第一次把 IndexedDB 的列表做成“最新在前”时,真正让我停下来查文档的不是异步回调,而是 openCursor() 第二个参数。把 prev 换成 next,结果会反过来;把对象仓库换成索引,还会遇到重复键和 nextunique 的选择。

官方地址:https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/openCursor

记住三个位置就够了:openCursor(query, direction) 的第一个参数限制“看哪些键”,第二个参数决定“从小到大还是从大到小”;只有索引可能出现重复索引键,才需要考虑 nextunique 或 prevunique。

先分清游标从哪里取数据

IndexedDB 的游标不是给一张抽象表随便排序,而是从一个具体的数据源开始。对 IDBObjectStore 打开游标时,记录按主键排列;对 IDBIndex 打开游标时,记录按索引键排列。这个差别决定了你应该观察 cursor.key、cursor.primaryKey 还是 cursor.value。

IndexedDB 的 IDBObjectStore、IDBIndex、IDBKeyRange 与 openCursor 方向关系静态说明图
图1:游标读取边界说明图。数据源、键范围和方向共同决定游标看到的记录;这是静态关系图,不是运行截图。

我在代码里通常先把这两个来源写成明确的变量名,避免后面误以为“索引键就是主键”:store.openCursor() 看的是主键顺序,store.index("userId").openCursor() 看的是 userId 索引顺序。

用 IDBKeyRange 限定“看哪些键”

query 是 openCursor() 的第一个参数。它可以是单个键,也可以是 IDBKeyRange。例如只读某个时间区间时,可以把起止键交给范围对象,而不是在回调里先取出全部记录再用 JavaScript 过滤。

const tx = db.transaction("posts", "readonly");
const store = tx.objectStore("posts");

// 只读取 createdAt 在起止时间内的记录,false 表示两端都包含。
const timeRange = IDBKeyRange.bound(startTime, endTime, false, false);

// null 表示不额外限制范围;第二个参数才是遍历方向。
const request = store.openCursor(timeRange, "prev");
request.onsuccess = (event) => {
  const cursor = event.target.result;
  if (!cursor) {
    // 没有下一条匹配记录,游标遍历结束。
    return;
  }

  renderPost(cursor.value);
  // 不传键时,沿着 prev 方向移动到下一条记录。
  cursor.continue();
};

这里最容易写反的是参数顺序:openCursor(timeRange, "prev") 表示“在范围内按降序遍历”,不是“先降序再把范围当成方向”。如果只想改变方向而不过滤,可以明确传 null:store.openCursor(null, "prev")。

IDBKeyRange.lowerBound()、upperBound() 和 bound() 的开闭边界也会影响第一条和最后一条记录。范围正确但方向不对,通常表现为列表顺序反了;方向正确但范围边界写错,通常表现为首尾多一条或少一条。

next 和 prev 决定顺序,但不会自动去重

next 从键范围起点向更大的键移动,保留遇到的记录;prev 从范围终点向更小的键移动,也保留遇到的记录。对对象仓库来说,主键本来就必须唯一,因此通常只在索引上明显看到“同一个索引键对应多条记录”。

方向排列方式重复索引键适合场景
next键升序全部返回从旧到新扫描、正向分页
prev键降序全部返回最新记录优先、反向分页
nextunique索引键升序每个索引键保留一条按索引键做去重读取
prevunique索引键降序每个索引键保留一条倒序按索引键取代表记录

所以“我用了 prev,为什么同一个用户还是出现多次?”的答案是:方向只改变游标移动方向,不承担去重职责。要不要去重,是另一层语义。

索引重复键时再考虑 nextunique 和 prevunique

假设 posts 有一个 userId 索引,同一个用户写入了多条文章。对这个索引调用 next,会看到同一个 userId 的多条记录;调用 nextunique,每个索引键只保留在该方向上最先遇到的记录。prevunique 同理,只是索引键从大到小遍历。

IndexedDB 索引重复键与 nextunique、prevunique、primaryKey 关系静态说明图
图2:索引重复键说明图。它展示方向上的唯一键选择与 primaryKey 的关系,不代表真实运行结果。

这里有一个很重要的边界:unique 不是“每次返回最新的一条”或“自动按时间字段去重”。它只围绕索引键去重,哪条记录被保留取决于索引游标的排序规则和方向。若业务想取“每个用户最新文章”,更稳妥的做法是建立能表达业务排序的索引,或在读取后明确比较时间字段,不要把 prevunique 当成“按时间取最新”的快捷方式。

const tx = db.transaction("posts", "readonly");
const store = tx.objectStore("posts");
const userIndex = store.index("userId");

// 只在索引重复键场景使用 unique 方向;它按 userId 去重。
const request = userIndex.openCursor(null, "nextunique");
request.onsuccess = (event) => {
  const cursor = event.target.result;
  if (!cursor) {
    // 每个索引键的代表记录都处理完了。
    return;
  }

  // key 是索引键,primaryKey 是对象仓库主键,value 是完整对象。
  showUserPost(cursor.key, cursor.primaryKey, cursor.value);
  cursor.continue();
};

如果你需要同一索引键下的全部记录,就应使用 next 或 prev,再自行决定如何合并;如果只需要每个索引键一个代表项,才考虑 unique 方向。另一个实用判断是:nextunique 和 prevunique 只能用于索引语义,不能拿来替代对象仓库的主键遍历。

把方向、范围和去重做成显式参数

我最后会把读取函数写成显式参数,调用点直接表达意图。这样以后从“全部记录倒序”改成“范围内正序”时,只改参数,不需要重写回调。

function scanIndex(index, range, direction, onRecord) {
  return new Promise((resolve, reject) => {
    // range 控制键范围,direction 控制顺序;两者不要混用。
    const request = index.openCursor(range ?? null, direction);

    request.onerror = () => reject(request.error);
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (!cursor) {
        // 结果为 null 表示当前事务中的游标已经遍历完毕。
        resolve();
        return;
      }

      try {
        onRecord(cursor);
        // 回调只处理当前记录,移动仍由游标负责。
        cursor.continue();
      } catch (error) {
        // 把业务回调异常转成 Promise 拒绝,避免静默中断。
        reject(error);
      }
    };
  });
}

// 例:按索引键降序读取一个范围,并保留重复索引键。
scanIndex(userIndex, IDBKeyRange.bound("a", "m"), "prev", (cursor) => {
  renderPost(cursor.value);
});

这段封装的重点不是 Promise,而是把三个决定拆开:数据源由传入的 index 决定,范围由 range 决定,顺序和是否去重由 direction 决定。若传入 nextunique,调用方还应该在函数名或注释中明确“按索引键去重”,避免读代码的人误以为是普通升序。

我会用这张速查表做选择

  • 想看全部记录且从小到大:next。
  • 想看全部记录且从大到小:prev。
  • 想限制首尾键:把 IDBKeyRange 放在第一个参数。
  • 想让索引键只出现一次:在索引上使用 nextunique 或 prevunique。
  • 想要“每个用户最新一条”:先确认索引排序能表达“最新”,不要只凭 prevunique 猜结果。

常见追问

把 direction 改成 prev 会改变数据库里的记录吗?

不会。direction 只决定当前游标怎样遍历,读取顺序改变,不会改写 object store、index 或记录主键。

nextunique 会按 value 去重吗?

不会。它针对索引键去重;对象内容相同但索引键不同,仍可能分别出现。需要按业务字段去重时,应显式设计索引或在应用层处理。

为什么范围正确,结果却还是有重复记录?

范围只负责限制键区间,不负责去重。若游标来自索引并且要合并同一索引键的多条记录,使用 next/prev 是预期行为;需要代表记录时再切换到对应的 unique 方向。

把 query 看成“范围”,把 direction 看成“顺序”,把 unique 看成“索引键是否只保留一个入口”,IndexedDB 游标的行为就不容易混在一起了。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>