第七部分:游标(Cursor)与范围查询

游标(Cursor)是 IndexedDB 中遍历数据的机制。与 getAll() 一次性加载所有数据不同,游标可以逐条处理记录,适合大数据集的遍历和复杂查询场景。

特性 游标 getAll
内存占用 低(逐条加载) 高(全部加载)
适用数据量 大数据集 小数据集
能否中途停止 不能
能否边遍历边修改 不能
使用复杂度 较高 简单
性能 适合大量数据 适合少量数据
// 基本语法
const request = store.openCursor();
const request = store.openCursor(query);
const request = store.openCursor(query, direction);
 
// 参数说明
// query: 主键值或 IDBKeyRange
// direction: 'next', 'nextunique', 'prev', 'prevunique'
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
 
const request = store.openCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    console.log('主键:', cursor.key);
    console.log('数据:', cursor.value);
 
    // 继续下一条
    cursor.continue();
  } else {
    console.log('遍历完成');
  }
};
// 从最后一条开始遍历
const request = store.openCursor(null, 'prev');
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    console.log('倒序 - 主键:', cursor.key);
    cursor.continue();
  }
};
// 等于某个值
IDBKeyRange.only(value);
 
// 大于等于 lower
IDBKeyRange.lowerBound(lower, open);
// open = true: 大于(不包含 lower)
// open = false: 大于等于(包含 lower)
 
// 小于等于 upper
IDBKeyRange.upperBound(upper, open);
 
// 在 lower 和 upper 之间
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen);
 
// 示例
IDBKeyRange.only(25);                    // = 25
IDBKeyRange.lowerBound(20);              // >= 20
IDBKeyRange.lowerBound(20, true);        // > 20
IDBKeyRange.upperBound(30);              // <= 30
IDBKeyRange.upperBound(30, true);        // < 30
IDBKeyRange.bound(20, 30);               // 20 <= x <= 30
IDBKeyRange.bound(20, 30, true, true);   // 20 < x < 30
IDBKeyRange.bound(20, 30, false, true);  // 20 <= x < 30
// 查询年龄在 20-30 之间的用户
const ageIndex = store.index('ageIndex');
const range = IDBKeyRange.bound(20, 30);
const request = ageIndex.openCursor(range);
 
// 查询名字以 "张" 开头的用户
const nameIndex = store.index('nameIndex');
const nameRange = IDBKeyRange.bound('张', '张\uffff');
const request2 = nameIndex.openCursor(nameRange);
 
// 查询创建时间在最近 7 天的记录
const dateIndex = store.index('createdAtIndex');
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000);
const dateRange = IDBKeyRange.lowerBound(sevenDaysAgo);
const request3 = dateIndex.openCursor(dateRange);
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const ageIndex = store.index('ageIndex');
 
// 按年龄排序遍历
const request = ageIndex.openCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    // cursor.key 是索引值(年龄)
    // cursor.primaryKey 是主键
    // cursor.value 是完整对象
    console.log(`年龄: ${cursor.key}, 用户ID: ${cursor.primaryKey}`);
    cursor.continue();
  }
};

键游标只遍历键,不加载完整对象,性能更好:

// 只获取键
const request = store.openKeyCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    console.log('键:', cursor.key);
    // cursor.value 为 undefined
    cursor.continue();
  }
};
 
// 索引键游标
const index = store.index('ageIndex');
const request2 = index.openKeyCursor();
 
request2.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    console.log('年龄:', cursor.key);
    console.log('主键:', cursor.primaryKey);
    cursor.continue();
  }
};
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
const request = store.openCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    const user = cursor.value;
 
    // 修改数据
    if (user.status === 'inactive') {
      user.lastLogin = new Date();
      user.status = 'active';
 
      // 更新当前记录
      const updateRequest = cursor.update(user);
      updateRequest.onsuccess = () => {
        console.log('更新成功:', user.id);
      };
    }
 
    cursor.continue();
  }
};
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
const request = store.openCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor) {
    const user = cursor.value;
 
    // 删除符合条件的记录
    if (user.createdAt < Date.now() - 365 * 24 * 3600 * 1000) {
      const deleteRequest = cursor.delete();
      deleteRequest.onsuccess = () => {
        console.log('删除成功:', user.id);
      };
    }
 
    cursor.continue();
  }
};
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
 
let count = 0;
const maxResults = 10;
 
const request = store.openCursor();
 
request.onsuccess = function(event) {
  const cursor = event.target.result;
 
  if (cursor && count < maxResults) {
    console.log(cursor.value);
    count++;
    cursor.continue();
  } else {
    console.log(`已获取 ${count} 条记录,遍历结束`);
    // 不调用 cursor.continue(),遍历自动停止
  }
};
function getPage(storeName, page = 1, pageSize = 20, options = {}) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction([storeName], 'readonly');
    const store = transaction.objectStore(storeName);
 
    let index = store;
    if (options.sortBy) {
      try {
        index = store.index(`${options.sortBy}Index`);
      } catch (e) {
        console.warn(`索引 ${options.sortBy}Index 不存在,使用主键排序`);
      }
    }
 
    const direction = options.sortOrder === 'desc' ? 'prev' : 'next';
    const skip = (page - 1) * pageSize;
 
    const results = [];
    let skipped = 0;
 
    const request = index.openCursor(null, direction);
 
    request.onsuccess = function(event) {
      const cursor = event.target.result;
 
      if (!cursor) {
        resolve(results);
        return;
      }
 
      if (skipped < skip) {
        skipped++;
        cursor.continue();
        return;
      }
 
      if (results.length < pageSize) {
        results.push(cursor.value);
        cursor.continue();
      } else {
        resolve(results);
      }
    };
 
    request.onerror = () => reject(request.error);
  });
}
 
// 使用
const page1 = await getPage('users', 1, 20, { sortBy: 'createdAt', sortOrder: 'desc' });
const page2 = await getPage('users', 2, 20, { sortBy: 'createdAt', sortOrder: 'desc' });

更高效的分页方式,使用上次的位置继续:

async function* paginate(storeName, pageSize = 20) {
  const transaction = db.transaction([storeName], 'readonly');
  const store = transaction.objectStore(storeName);
 
  let hasMore = true;
  let lastKey = null;
 
  while (hasMore) {
    const results = [];
    let count = 0;
 
    await new Promise((resolve, reject) => {
      let request;
      if (lastKey) {
        request = store.openCursor(IDBKeyRange.lowerBound(lastKey, true));
      } else {
        request = store.openCursor();
      }
 
      request.onsuccess = (event) => {
        const cursor = event.target.result;
 
        if (cursor && count < pageSize) {
          results.push(cursor.value);
          lastKey = cursor.key;
          count++;
          cursor.continue();
        } else {
          hasMore = !!cursor;
          resolve();
        }
      };
 
      request.onerror = () => reject(request.error);
    });
 
    yield results;
  }
}
 
// 使用
const pager = paginate('users', 50);
for await (const page of pager) {
  console.log('获取到', page.length, '条记录');
  // 处理数据...
}

本章介绍了游标的使用方式:

  • 游标适合大数据集的逐条遍历
  • IDBKeyRange 提供强大的范围查询能力
  • 索引游标可按非主键属性排序遍历
  • 可以在遍历过程中更新或删除记录
  • 分页查询可结合游标实现高效翻页

继续阅读 第八部分:数据库版本升级

该主题尚不存在

您访问的页面并不存在。如果允许,您可以使用创建该页面按钮来创建它。

  • indexeddb/part07-cursor.txt
  • 最后更改: 2026/04/27 19:52
  • 张叶安