IndexDB游标的正确打开方式

IndexDB游标的正确打开方式

引言

我最近在维护我的玩具项目react-door,当我试图使用IndexDB的游标遍历某个表时,我遇到了一些让人十分困惑的问题,下面我将带大家来复现一下事故的现场。

复现

首先,在我的印象中,游标类似迭代器,所以刚开始我试图拿到游标传递出去然后在dao层调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// indexDB.js
// 获取cursor
const getCursor = () => {
if (!db) {
return Promise.reject("database connect instance can't be null")
}
const store = db.transaction(storeName, 'readonly').objectStore(storeName)
const request = store.openCursor();
return new Promise((resolve, reject) => {
request.onsuccess = (e) => {
const cursor = e.target["result"]
resolve(cursor)
}
request.onerror = (e) => {
reject(e)
}
})
}


// in dao.js
async function someFunc() {
const cursor = await getCursor();
if (cursor) {
// do something
cursor.continue()
} else {
console.log("no more data")
}
}

一开始我是这么设想的,但是我发现了一个令人特别疑惑的问题:为什么在MDN的文档中,使用if...else...来遍历这个cursor呢?

1
2
3
4
5
6
7
8
9
10
if (cursor) {
var listItem = document.createElement("li");
listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year;
list.appendChild(listItem);
cursor.continue();
} else {
console.log("Entries all displayed.");
}

// 来自MDN,可以看到使用 if else 进行遍历

当时我也是借鉴MDN,使用if...else...来遍历,但是我发现我的代码只能运行一次,这肯定不叫遍历啊。谁家好人用if...else做遍历啊??? 年轻人不气盛能叫年轻人吗?这肯定是MDN写错了!根据我的经验,立刻给他改写成了这样:

1
2
3
4
5
6
7
8
9
10
11
async function someFunc() {
const cursor = await getCursor();
// 从 if 语句变为 while 语句
while (cursor) {
// 猜猜会输出什么?
console.log(cursor.key);
// do something
cursor.continue()
}
console.log("no more data")
}

看起来没什问题,非常正确!

但是 ,在我实际运行这段代码的时候,并不能正常运行。我们希望它的输出是1 2 3 4 5 ....,而实际的输出结果却是1 1 1 1 1 ...。为啥会出现这种结果呢?😥本来我以为是cursor.continue()方法是异步的,但是结果这是一个完完全全的同步函数。怎么回事呢?

如果说continue方法不起作用的话,但是它确实能够帮我们将cursor向前移位,并在cursor结束时结束循环(虽然报了一个info),但是如果说它起作用的,我们的cursor每次都指向第一条数据。这是怎么回事呢???

难道IndexDB这个方法还有bug?不可能吧?难道真的是使用if...else遍历?于是我再次查阅MDN,现在我为大家贴上MDN完整的代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function displayData() {
var transaction = db.transaction(["rushAlbumList"], "readonly");
var objectStore = transaction.objectStore("rushAlbumList");

objectStore.openCursor().onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var listItem = document.createElement("li");
listItem.innerHTML = cursor.value.albumTitle + ", " + cursor.value.year;
list.appendChild(listItem);

cursor.continue();
} else {
console.log("Entries all displayed.");
}
};
}

经过我的观察,难道…cursor只能在openCursor的回调中使用?? 带着这个猜想,我将我的代码改为这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!db) {
return Promise.reject("database connect instance can't be null")
}
const store = db.transaction(storeName, 'readonly').objectStore(storeName)
const request = store.openCursor();
return new Promise((resolve, reject) => {
request.onsuccess = (e) => {
const res = []
const cursor = e.target["result"]
if (cursor) {
res.push(cursor.value)
cursor.continue()
} else {
resolve(res)
}
}
request.onerror = (e) => {
reject(e)
}
})

嗯,果然不出所料,现在我们已经可以正确的遍历cursor了!但是又有一个问题随之而来,为什么每次我的res只有一个数据😅?经过我的一番思考,一个很恐怖的想法闪现出来,沃日,indexDB你不会….每次游标移位都要调用onSuccess回调吧

于是我们再次改写代码,写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!db) {
return Promise.reject("database connect instance can't be null")
}
const store = db.transaction(storeName, 'readonly').objectStore(storeName)
const request = store.openCursor();
// 将res放到回调函数之外
const res = []
return new Promise((resolve, reject) => {
request.onsuccess = (e) => {
const cursor = e.target["result"]
if (cursor) {
res.push(cursor.value)
cursor.continue()
} else {
resolve(res)
}
}
request.onerror = (e) => {
reject(e)
}
})

这时,我们终于如愿以偿的拿到了我们需要的数据(真是辛苦啊😭)

总结

现在我们来总结一下cursor的坑:

  • cursor必须配合oepnCursor函数的回调函数一起使用

  • 每次调用cursor.continue()并不是简单的将cursor移位,而是请求IDB的API更新cursor并再次触发openCursor的回调函数

所以上面那一版代码就是使用cursor遍历的标准模板,因为这个过程是类似递归的过程,所以我们的cursor是真的使用if...else...遍历代码😥,没毛病哦兄弟们