IndexedDB事务和Promise之间的相互影响不一致

dum*_*ter 10 javascript promise indexeddb bluebird

我在Reddit上看到了sync-promise,并与作者进行了讨论.我们注意到IndexedDB事务和promise之间的关系有一些奇怪的不一致.

所有onsuccess事件完成后,IndexedDB事务会自动提交.一个复杂的问题是,onsuccess除了在同一个事务上执行另一个操作之外,您不能在回调中执行任何异步操作.例如,您无法在a中启动AJAX请求onsuccess,然后在AJAX请求返回某些数据后重用相同的事务.

承诺与它有什么关系?据我了解,承诺解析应该始终是异步的.这意味着如果不自动提交IndexedDB事务,则无法使用promises.

这是我正在谈论的一个例子:

var openRequest = indexedDB.open("library");

openRequest.onupgradeneeded = function() {
  // The database did not previously exist, so create object stores and indexes.
  var db = openRequest.result;
  var store = db.createObjectStore("books", {keyPath: "isbn"});
  var titleIndex = store.createIndex("by_title", "title", {unique: true});
  var authorIndex = store.createIndex("by_author", "author");

  // Populate with initial data.
  store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
  store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
  store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});
};

function getByTitle(tx, title) {
  return new Promise(function(resolve, reject) {
    var store = tx.objectStore("books");
    var index = store.index("by_title");
    var request = index.get("Bedrock Nights");
    request.onsuccess = function() {
      var matching = request.result;
      if (matching !== undefined) {
        // A match was found.
        resolve(matching);
      } else {
        // No match was found.
        console.log('no match found');
      }
    };
  });
}

openRequest.onsuccess = function() {
  var db = openRequest.result;
  var tx = db.transaction("books", "readonly");
  getByTitle(tx, "Bedrock Nights").then(function(book) {
    console.log('First book', book.isbn, book.title, book.author);
    return getByTitle(tx, "Quarry Memories");
  }).then(function(book) {
    console.log('Second book', book.isbn, book.title, book.author);
    // With native promises this gives the error:
    // InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable
    // With bluebird everything is fine
  });
};
Run Code Online (Sandbox Code Playgroud)

(完全披露:演示是由paldepind创建的,而不是我!)

我在Chrome和Firefox中尝试过它.由于事务自动提交,它在Firefox中失败,但它实际上在Chrome中有效!哪种行为是正确的?如果Firefox的行为是正确的,那么使用IndexedDB事务使用"正确"的承诺实际上是不可能的吗?

另一个复杂因素:如果我在运行上面的演示之前加载bluebird,它可以在Chrome和Firefox中运行.这是否意味着蓝鸟同步解决承诺?我以为不应该这样做!

的jsfiddle

Dom*_*nic 9

这可能是由于微任务和任务("macrotasks")之间的差异.Firefox从来没有使用微任务的标准投诉承诺实现,而Chrome,Bluebird和其他人正确使用微任务.你可以看到这一点,微任务(比macrotask"更早"执行,但仍然是异步)落在事务边界内,而macrotask(例如来自Firefox的承诺)却没有.

所以,这是一个Firefox错误.

  • 这在Firefox 60版本中已修复.请参阅[bug 1193394](https://bugzilla.mozilla.org/show_bug.cgi?id=1193394). (3认同)

pal*_*ind 7

好的,所以我再次深入研究了IndexedDB,DOM和HTML规范.我真的需要为SyncedDB做到这一点,因为它在很大程度上依赖于事务中的承诺.

问题的关键在于延迟执行onFulfilled和Promises/A +兼容的onRejected回调then是否必须展示将触发IndexedDB事务提交.

当您从规范中提取并排列它们时,事务生命周期的IndexedDB规则实际上非常简单:

这大致意味着:

  • 创建事务时,您可以根据需要放置任意数量的请求.
  • 从那时起,新请求只能在事件处理程序内为另一个请求successerror事件监听器进行.
  • 当所有请求都已执行且未发出新请求时,事务将提交.

接下来的问题是:如果一个承诺是一个内部实现requestsuccesserror事件监听器将其onFulfilled在IndexedDB的再次设定交易为无效之前回调被调用?即onFullfilled,在发布成功事件时,将回调作为步骤3的一部分进行调用?

该步骤调度一个事件,IndexedDB使用DOM事件,因此执行的实际操作超出了IndexedDB规范.相反,在DOM规范中指定了调度事件的步骤.越过这些步骤,很明显,在任何时候都不会执行微任务(可以调用promise回调)检查点.因此,最初的结论是,在onFulfilled调用任何回调之前,事务将被关闭.

但是,如果我们通过onsuccessrequest对象上指定属性来附加事件侦听器,则事情变得更加毛茸茸.在这种情况下,我们不是简单地根据DOM规范添加事件监听器.我们正在设置HTML规范中定义的事件处理程序IDL属性.

当我们这样做时,回调不会直接添加到事件侦听器列表中.相反,它"包装"在事件处理程序处理算法中.该算法执行以下重要操作:

  1. 在步骤3中,它运行跳转到代码入口点算法.
  2. 这则执行步骤来运行回调后清理
  3. 最后,这将执行微任务检查点.这意味着在事务被标记为非活动之前,将调用您的promise回调!欢呼!

这是个好消息!但是,答案取决于您是否success通过使用addEventListener或设置onsuccess事件处理程序来监听事件,这很奇怪.如果您执行前者,onFulfilled则在调用promise的回调时,事务应处于非活动状态,如果执行后者,则应该仍处于活动状态.

然而,我无法重现现有浏览器的差异.使用原生承诺Firefox无论如何都会在示例代码中失败,即使使用Chrome也会成功addEventListener.我有可能忽略或误解了规范中的某些内容.

作为最后一点,Bluebird承诺将关闭Internet Explorer 11中的事务.这是由于Bluebird在IE中使用的调度.我的同步承诺实现在IE中的事务内部工作.


PJ *_*Eby 5

你是对的:承诺是异步解决的,IndexedDB 有一些同步要求。虽然其他答案指出本机承诺可能在某些浏览器的某些版本中与 IndexedDB 一起正常工作,但作为一个实际问题,您可能必须处理它在您所针对的某些浏览器中不起作用的问题。

然而,使用同步承诺实现是一个可怕的想法。Promise 是异步的,这是有充分理由的,如果让它们同步,则会引入不必要的混乱和潜在的错误。

然而,有一个相当简单的解决方法:使用 Promise 库,它提供了一种显式刷新其回调队列的方法,以及一个 IndexedDB 包装器,它在调用事件回调后刷新Promise回调队列。

从 Promises/A+ 的角度来看,在事件结束时调用的处理程序之间或在下一个滴答周期开始时调用的处理程序之间没有任何区别——它们仍然在设置完所有代码之后被调用回调已经完成,这是 Promise 异步的重要部分。

这允许您使用异步的承诺,在满足所有 Promises/A+ 保证的意义上,但仍确保 IndexedDB 事务不会关闭。因此,您仍然可以获得不会“一次性”发生的回调的所有好处。

当然,问题在于您需要支持这一点的库,并不是每个 Promise 实现都公开了一种指定调度程序或刷新其回调队列的方法。同样,我不知道有任何支持此功能的开源 IndexedDB 包装器。

但是,如果您正在使用 Promsies 编写自己的 IndexedDB 包装器,那么最好使用适当的 Promise 实现,并相应地刷新其回调队列。一个简单的选择是嵌入许多只有 100 行左右的 Javascript 的“微承诺”实现之一,并根据需要对其进行修改。或者,使用具有自定义调度支持的较大的主流 Promise 库之一也是可行的。

千万不能使用同步的承诺库,同步蓝鸟构建,或同步调度。如果你这样做,你不妨完全放弃承诺并使用直接回调。

后续注意事项:一位评论者建议同步承诺与刷新回调队列一样安全。但他们错了。可怕的,可怕的错误。您可以很好地推理单个事件处理程序,以说“这里没有任何其他代码在运行;现在可以调用回调”。要对同步 Promise 进行类似的分析,需要完全了解一切如何调用其他一切……这与您首先想要 Promise 的原因正好相反。

在具体的同步承诺实现中,同步承诺作者声称他们的承诺库现在是“安全的”,并且不会“发布 Zalgo”。他们再次错了:它不安全,并且确实释放了 Zalgo。作者显然并没有真正理解关于“释放 Zalgo”的文章,并且成功地重新实现了jQuery promises,人们普遍认为由于多种原因,包括它们的 Zalgo 特性,这些承诺被广泛认为是非常糟糕的。

无论您的实现如何,同步承诺都不安全。