游标(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, '条记录'); // 处理数据... }
本章介绍了游标的使用方式:
继续阅读 第八部分:数据库版本升级。