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

时间:2015-02-07 22:19:14

标签: javascript promise indexeddb bluebird

我在Reddit和sync-promise上发布了got into a discussion with the author。我们注意到IndexedDB事务和promise之间的关系存在一些奇怪的不一致。

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

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

Here is an example of what I'm talking about:

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
  });
};

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

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

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

JSFiddle

3 个答案:

答案 0 :(得分:9)

这可能是由于the difference between microtasks and tasks ("macrotasks")。 Firefox从来没有使用微任务的标准投诉承诺实现,而Chrome,Bluebird和其他人正确使用微任务。你可以看到这一点,微任务(执行#34;比宏任务更早,但仍然是异步)落在事务边界内,而macrotask(例如来自Firefox的承诺)却没有。 / p>

所以,这是一个Firefox错误。

答案 1 :(得分:7)

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

问题的关键在于,承诺Promise / A +必须展示的onFulfilledonRejected回调到then的延迟执行是否会触发IndexedDB事务提交。

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

这大致转换为:

  • 创建交易时,您可以根据需要对其进行多次请求。
  • 从那时起,新请求只能在其他请求successerror事件监听器的事件处理程序中进行。
  • 当所有请求都已执行且没有新请求时,事务将提交。

接下来的问题是:如果在request successerror事件监听器中履行了承诺,则会在onFulfilled之前调用其onFullfilled回调。 IndexedDB再次将事务设置为非活动状态?即将onFulfilled回调作为firing a success event中的第3步的一部分进行调用?

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

但是, if 我们通过在request对象上指定success属性来附加事件侦听器,事情变得更加毛茸茸。在这种情况下,根据DOM规范,我们不仅仅是adding an event listener。我们正在设置HTML规范中定义的event handler IDL attribute

当我们这样做时,回调不会直接添加到事件侦听器列表中。而是#34;包裹"在the event handlers processing algorithm内。该算法执行以下重要操作:

  1. 在第3步中,它会运行jump to code entry-point algorithm
  2. 然后执行clean up after running a callback
  3. 的步骤
  4. 最后,这会执行microtask checkpoint。这意味着在事务被标记为非活动之前,将调用您的promise回调!万岁!
  5. 这是个好消息!但是,答案取决于您是否使用addEventListener或设置onsuccess事件处理程序来监听onFulfilled事件,这很奇怪。如果您执行前者,则在您的承诺的addEventListener回调被调用时,该交易应处于非活动状态,如果您执行此操作,则该事务仍应处于活动状态。

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

    作为最后一点,Bluebird承诺将关闭Internet Explorer 11中的事务。这是由于Bluebird在IE中使用的调度。我的synchronized promise implementation适用于IE中的交易。

答案 2 :(得分:4)

您是对的:Promise是异步解析的,而IndexedDB有一些同步要求。虽然其他答案指出本机承诺可能在某些浏览器的某些版本中与IndexedDB一起正常工作,但实际上您可能必须处理它在某些浏览器中无法正常工作的问题。定位。

然而,使用同步承诺实现是一个可怕的想法。 Promise是非常好的原因,如果你让它们同步,你就会引入不必要的混乱和潜在的bug。

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

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

这允许您使用异步的promises,在满足所有Promises / A +保证的意义上,但仍然确保In​​dexedDB事务不会被关闭。因此,你仍然可以获得所有不会发生回调的好处,而且#34;所有这些都会立刻产生#34;。

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

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

使用同步承诺库,同步Bluebird构建或同步调度程序。如果你这样做,你可能完全放弃承诺并使用直接回调。

后续注释:一位评论者建议同步承诺与刷新回调队列一样安全。但他们错了。可怕,可怕的错误。你可以推理一下单一事件处理程序,足以说明"这里没有运行任何其他代码;现在可以调用回调"要使用同步承诺进行类似的分析,需要完全理解所有内容如何调用其他内容......这与您首先想要承诺的原因完全相反。

在特定的同步承诺实施中,同步承诺作者声称他们的承诺库现在是安全的"并且没有"释放Zalgo"。它们再一次是错的:它不安全, 发布Zalgo。作者显然并没有真正理解有关发布Zalgo"的文章,并成功重新实现了 jQuery promises ,由于多种原因(包括他们的Zalgo-)而被广泛认为是可怕的。岬。

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