indexedDB事務功能的Promise化封裝

前言

本文是介紹我在編寫indexedDB封裝庫中誕生的一個副產品——如何讓indexedDB在支持鏈式調用的同時,保持對事務的支持。
項目地址:https://github.com/woodensail/indexedDBgit

2015/11/12 注:這篇文章的思路有問題,你們看看了解一下就行,不要這麼幹。更好的作法已經寫在了下一篇中。你們能夠去看一下,順便幫忙點個推薦或者收藏一個。
地址:http://segmentfault.com/a/1190000003984871es6

indexedDB的基本用法

var tx = db.transaction('info', 'readonly');
var store = tx.objectStore('info');
store.get('id').onsuccess = function (e) {
    console.log(e.target.result);
};

上面這段代碼中,開啓了一個事務,並從名爲info的store中取得了key爲id的記錄,並打印在控制檯。
其中打印的部分寫在了onsuccess回調中,若是咱們但願把取出的id加1而後返回就須要這樣:github

// 方案1
var tx = db.transaction('info', 'readwrite');
var store = tx.objectStore('info');
store.get('id').onsuccess = function (e) {
    store.put({key:'id',value:e.target.result.value + 1}).onsuccess = function (e) {
        ……
    };
};

// 方案2
var tx = db.transaction('info', 'readwrite');
var store = tx.objectStore('info');
var step2 = function(e){
    store.put({key:'id',value:e.target.result.value + 1}).onsuccess = function (e) {
        ……
    };
}
store.get('id').onsuccess = step2;

前者用到了嵌套回調,後者則須要將業務流程拆散。
綜上,對indexedDB進行必定的封裝,來簡化編碼操做。chrome

Promise化的嘗試

對於這種帶大量回調的API,使用Promise進行異步化封裝是個好主意。
咱們能夠作以下封裝:數據庫

function put(db, table, data ,tx) {
    return new Promise(function (resolve) {
        var store = tx.objectStore(table);
        store.put(data).onsuccess = function (e) {
            resolve(e);
        };
    });
}

var tx = db.transaction('info', 'readwrite');
Promise.resolve().then(function(){
    put(db, 'info', {……}, tx)
}).then(function(){
    put(db, 'info', {……}, tx)
});

看上去這麼作是沒有問題的,可是實質上,在存儲第二條數據時,會報錯並提示事務已被中止。segmentfault

事務與Promise的衝突

When control is returned to the event loop, the implementation MUST set the active flag to false.數組

——摘自W3C推薦標準(W3C Recommendation 08 January 2015)promise

如同上面的引用所說,目前的W3C標準要求在控制權回到事件循環時,當前開啓的事務必須被設置爲關閉。所以包括Promise.then在內的全部異步方法都會強制停止當前事務。這就決定了一個事務內部的全部操做必須是同步完成的。
也真是基於這個緣由,我沒有在github上找到實現鏈式調用的indexedDB封裝庫。
其中寸志前輩的BarnJS中到是有鏈式調用,然而只是實現了Event.get().then()。也就是隻能一次數據庫操做,一次結果處理,而後就結束。並不能串聯多個數據庫操做在同一個事務內。緩存

不夠要實現鏈式調用其實也不難,關鍵的問題就在於Promise自己是爲異步操做而生的,所以會在鏈式調用的各個函數中返回事件循環,從而減小網頁的卡頓。因此咱們就須要實現一個在執行每一個函數過程之間不會返回事件循環的Promise,也就是一個同步化的Promise。異步

也許是這個要求太過奇葩,我沒發現網上有提供同步化執行的promise庫。因此只能本身實現一個簡單的。雖然功能不全,但也能湊活用了。下面是使用樣例和詳細代碼解釋,完整代碼見github。

使用樣例

// 這句是我封裝事後的用法,等效於:
// var tx = new Transaction(db, 'info', 'readwrite');
var tx = dbObj.transaction('info', 'readwrite');

//正常寫法
tx.then(function () {
    tx.get('info', 'a');
    tx.get('info', 'b');
}).then(function (a, b) {
    tx.put('info', {key : 'c', value : Math.max(a.v, b.v));
})

//偷懶寫法
tx.then(function () {
    tx.getKV('info', 'a');
    tx.getKV('info', 'b');
}).then(function (a, b) {
    tx.putKV('info', 'c',  Math.max(a, b));
})

代碼解釋

var Transaction = function (db, table, type) {
    this.transaction = db.transaction(table, type);
    this.requests = [];
    this.nexts = [];
    this.errorFuns = [];
};
Transaction.prototype.then = function (fun) {
    var _this = this;
    // 若errored爲真則視爲已經出錯,直接返回。此時後面的then語句都被放棄。
    if (this.errored) {
        return this;
    }
    // 若是當前隊列爲空則將自身入隊後,馬上執行,不然只入隊,不執行。
    if (!_this.nexts.length) {
        _this.nexts.push(fun);
        fun(_this.results);
        _this.goNext();
    } else {
        _this.nexts.push(fun);
    }
    // 返回this以實現鏈式調用
    return _this;
};

Transaction的初始化語句和供使用者調用的then語句。


Transaction.prototype.put = function (table, data) {
    var store = this.transaction.objectStore(table);
    this.requests.push([store.put(data)]);
};
Transaction.prototype.get = function (table, key) {
    var store = this.transaction.objectStore(table);
    this.requests.push([store.get(key)]);
};
Transaction.prototype.putKV = function (table, k, v) {
    var store = this.transaction.objectStore(table);
    this.requests.push([store.put({k, v})]);
};
Transaction.prototype.getKV = function (table, key) {
    var store = this.transaction.objectStore(table);
    this.requests.push([store.get(key), item=>(item || {}).v]);
};

全部的函數都在發起數據庫操做後將返回的request對象暫存入this.requests中。
目前只實現了put和get,其餘的有待下一步工做。另外,getKV和setKV是專門用於存取key-value數據的,要求被存取的store包含k,v兩個字段,其中k爲主鍵。


// 該語句會在鏈式調用中的每一個函數被執行後馬上調用,用於處理結果,並調用隊列中的下一個函數。
Transaction.prototype.goNext = function () {
    var _this = this;
    // 統計前一個函數塊中執行的數據庫操做數量
    var total = _this.requests.length;
    // 清空已完成數據庫操做計數器
    _this.counter = 0;
    // 定義所有操做執行完畢或出差後的回調函數
    var success = function () {
        // 當已經有錯誤出現時,放棄下一步執行
        if (_this.errored) {
            return;
        }
        // 將隊首的節點刪除,也就是剛剛執行完畢的節點
        _this.nexts.shift();
        _this.requests = [];
        // 從返回的event集合中提取出全部result,若是有parser則使用parser。
        _this.results = _this.events.map(function (e, index) {
            if (_this.parser[index]) {
                return _this.parser[index](e.target.result);
            } else {
                return e.target.result;
            }
        });
        
        //判斷隊列是否已經執行完畢,不然繼續執行下一個節點
        if (_this.nexts.length) {
            // 將節點的執行結果做爲參數傳給下一個節點,使用了spread操做符。
            _this.nexts[0](..._this.results);
            _this.goNext();
        }
    };
    // 初始化events數組,清空parser存儲器
    _this.events = new Array(total);
    _this.parser = {};

    // 若該請求內不包含數據庫操做,則視爲已完成,直接調用success
    if (total === 0) {
        success();
    }

    // 對於每一個請求將請求附帶的parser放入存儲區。而後綁定onsuccess和onerror。
    // 其中onsuccess會在每一個請求成功後將計數器加一,當計數器等於total時執行回調
    _this.requests.forEach(function (request, index) {
        _this.parser[index] = request[1];
        request[0].onsuccess = _this.onsuccess(total, index, success);
        request[0].onerror = _this.onerror;
    })
};
Transaction.prototype.onsuccess = function (total, index, callback) {
    var _this = this;
    return function (e) {
        // 將返回的event存入event集合中的對應位置
        _this.events[index] = e;
        _this.counter++;
        if (_this.counter === total) {
            callback();
        }
    }
};
Transaction.prototype.onerror = function (e) {
    // 設置錯誤標準
    this.errored = true;
    // 保存報錯的event
    this.errorEvent = e;
    // 一次調用全部已緩存的catch函數
    this.errorFuns.forEach(fun=>fun(e));
};
Transaction.prototype.cache = function (fun) {
    // 若是已設置錯誤標準則用緩存的event爲參數馬上調用fun,不然將其存入隊列中
    if (this.errored) {
        fun(this.errorEvent);
    } else {
        this.errorFuns.push(fun);
    }
};

核心的goNext語句以及success與error的回調。catch相似then用於捕捉異常。

總結

好累啊,就這樣吧,之後再加其餘功能吧。另外這裏面用了很多es6的寫法。因此請務必使用最新版的edge或chrome或firefox運行。或者你能夠手動把es6的寫法都去掉。

相關文章
相關標籤/搜索