indexedDB事务功能的Promise化封装
前言
本文是介绍我在编写indexedDB封装库中诞生的一个副产品——如何让indexedDB在支持链式调用的同时,保持对事务的支持。
项目地址:https://github.com/woodensail/indexedDB
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然后返回就需要这样:
// 方案1var 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) { …… };};// 方案2var 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进行一定的封装,来简化编码操作。
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)});
看上去这么做是没有问题的,但是实质上,在存储第二条数据时,会报错并提示事务已被停止。
事务与Promise的冲突
When control is returned to the event loop, the implementation MUST set the active flag to false.
——摘自W3C推荐标准(W3C Recommendation 08 January 2015)
如同上面的引用所说,目前的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的写法都去掉。