IndexDB游标的正确打开方式


IndexDB游标的正确打开方式

引言

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

复现

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

// 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呢?

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写错了!根据我的经验,立刻给他改写成了这样:

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完整的代码段:

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的回调中使用?? 带着这个猜想,我将我的代码改为这样:

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回调吧

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

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...遍历代码😥,没毛病哦兄弟们

发布于:
编辑于: