为什么在声明事件处理程序之前发出 indexedDB 请求?

Why are indexedDB requests made before the event handlers are declared?

感觉这一定是个愚蠢的问题,但我不明白在 indexedDB 中发出请求的一些基本知识。

为什么在定义事件处理程序之前发出请求?例如,request = objectStore.add(data) 是在 request.onsuccessrequest.onerror 函数声明之前创建的。这个对吗?是否有可能在注册事件处理程序之前完成请求?

我将其与创建图像元素进行比较,然后声明 onload 和 onerror 的事件处理程序,所有这些都在将 source 属性设置为文件位置并尝试加载它之前。但是请求"element"不能在请求发出前创建;因此,在发出请求之前,没有任何东西可以附加事件。

请让我知道我在这里遗漏了什么。我一直在毫无问题地从 indexedDB 中写入和检索数据,并且认为我一直在正确编码;但我想确保这是正确的并且永远有效。

谢谢。

重复响应

我以前读过那个问题和答案,当我第一次开始阅读有关 indexedDB 的内容时,我完全忘记了它。如果我在写这个问题之前再次找到它,我可能不会提交它,并且会接受代码应该解决我是否理解它的问题。处理错误事件和事务中止是让我再次考虑语句顺序的原因。

但是,在再次阅读答案之后,我的理解还不够深入,只能接受它并希望它永远有效。我不是想挖苦人。从某种意义上说,我的能力有限,无法思考事件循环和时代,而且所有事情都是同时发生的,这让我感到困惑。

At the end of the epoch (or the start of the next, whatever you think is easier to understand), the underlying JS engine goes back and looks at what is registered to execute, and then executes everything nearly all at once.

必须有一个执行顺序,否则没有任何意义,异步与否。我知道解释器在开始执行下一行代码之前不会等待任何同步进程完成。但是同步语句不是按照它们在代码中出现的顺序完全依次处理,异步语句按照它们在代码中出现的顺序开始,这样如果异步过程很快出错,如果事件处理程序没有提前声明?事件处理程序不像函数声明那样被提升,是吗?这是我仍然感到困惑的部分。

在 Jake Archibald 的这篇 article 承诺中,他在介绍中提供了一个有关图像加载的示例并写道:

Unfortunately, in the example above, it's possible that the events happened before we started listening for them, so we need to work around that using the "complete" property of images.

This doesn't catch images that error'd before we got a chance to listen for them; unfortunately the DOM doesn't give us a way to do that. Also, this is loading one image, things get even more complex if we want to know when a set of images have loaded.

这给人的印象是顺序很重要,因此,在图像的情况下,如果可能的话,应该在声明所有事件处理程序之后分配源,以免错过听证会事件。对我来说重要的部分是事件可以在事件处理程序 declared/registered.

之前发生。

在 indexedDB 中声明事件处理程序后,我尝试遵循相同的模式来发出请求,但这似乎不可能,因为在发出请求之前没有任何事件可以附加到。

即使所有语句都是异步的,例如 MDN Web 文档中关于使用 IndexedDB 的 this 示例,有些事情仍然相当混乱。 objectStore.transaction.oncomplete 是一个有趣的声明。在尝试向其中写入数据之前,我们正在等待创建 objectStore。 (我认为这被认为是不好的做法,在 onupgradeneeded 事件中写入数据;因此,我们不使用该语句。)但令人困惑的是为什么我们不担心在创建索引之前创建的 objectStore。为什么 createIndex 语句不在 createObjectStore 语句启动的同时启动,如果所有内容都同时处理?如果 createObjectStore 语句在 createIndex 语句开始之前没有完成,是否应该需要事件处理程序,否则它会因为 objectStore 尚不存在而失败?

我知道它有效,因为我一直在使用相同的代码模式,但我真的不明白。

我想更好地理解这两项——错过事件的可能性以及为什么在此 indexedDB 示例中不需要事件处理程序。我不知道这是否让我的问题有所不同,但重复问题的答案并没有为我回答这些问题。也许,我必须更好地了解 JS 引擎才能理解这些问题的答案。

const dbName = "the_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // Handle errors.
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique - or at least that's what I was told during the kickoff meeting.
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Use transaction oncomplete to make sure the objectStore creation is 
  // finished before adding data into it.
  objectStore.transaction.oncomplete = function(event) {
    // Store values in the newly created objectStore.
    var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
    customerData.forEach(function(customer) {
      customerObjectStore.add(customer);
    });
  };
};

Clarification/Response 至 Answer/Comments

感谢您花时间回答我的问题并补充说明。

首先,"before" 我只是指语句在脚本中出现的顺序。

我想我遵循了你的类比,这是一个很好的类比。我还是不明白为什么员工要等到第二天才把他们的工作提交给秘书,保证秘书会在那里接收。

这听起来类似于 javascript 解释器,当它执行相当于编译脚本时,提升函数声明,这样就可以在函数声明之前在代码中调用函数已制作。

看来你的说法,用我的简单术语重申,JS 引擎在最终执行之前的某个时刻,分配事件处理程序(秘书)在比其中的事件处理程序更早的时期注册。最终触发事件的请求(员工)将完成。因此,请求语句相对于事件处理程序出现在代码中的什么位置并不重要,也就是说,只要它们是在同一时期内定义的即可。

JS 引擎不知道请求何时完成,只知道事件处理程序已注册开始侦听以及请求何时开始。只要 JS 引擎有一个过程来正确地对这些步骤进行排序,而与语句在代码中出现的顺序无关,这样就不会错过一个事件,那么它对我来说与函数声明的提升没有什么不同,我不会为了完成我的任务,真的需要再多考虑一下了。

不过,我还是想更好地理解什么是纪元,至少要知道这些陈述是在同一个纪元内做出的。我在 MDN Web 文档中关于 "Concurrecny Model and Event Loop" 的文章中没有看到任何时代的提及。你介意给我指点你知道的任何好的资源吗?

谢谢。

最后的笔记

我通过 link 遇到了这两个项目,在这里,堆栈溢出。八年前有人问过同样的问题,回答的方式大致相同,只是用了不同的术语;也就是说,不是纪元,而是 javascript 代码将 "run to completion" 或具有 运行 完成语义。这个 question refers you to this document 可以搜索 "run to completion" 阅读两个关于为什么在注册事件处理程序之前发出请求的设置中没有竞争条件的交流。我有一本 David Flanagan 的旧 JavaScript 书,在讨论 JS "programs," 的执行时指出,因为 JS 具有单线程执行,所以永远不必担心竞争条件;但不知道他说的是不是这个情况。

因此,这个问题在过去已经被问过和回答过多次,我想我只是另一个新手问一个老问题,好像我是第一个想到的,而且没有足够的知识如何JS 进程。

上面 "Concurrency Model and Event Loop"、link 的文章有一个简短的 "Run-to-completion" 部分;但直到阅读上文 linked 的最后一篇文档后,我才明白它的含义。

我现在的意思是一个函数中的所有代码都将 运行 在任何其他代码开始之前完成,这似乎有两种解释。

  1. 一个是当函数代码中的语句到达时,对数据库的异步请求排队,但直到函数中的所有其他语句都到达时才真正开始运行 ,包括之后声明的事件处理程序。

  2. 或者,根据上面最后 linked 的文档,异步请求可能 运行 甚至在事件处理程序注册之前完成,但是完成通知将保留在队列中,直到函数中的其余语句 运行 并且事件处理程序已注册后才会执行。

解释 2 似乎是准确的,但是,无论哪种情况都是实际情况,现在对我来说这一切都很有意义,并且解释了为什么在员工提交工作之前秘书总是在那里,以及为什么,即使员工在纳秒内完成工作,直到第二天保证秘书在场接收时,员工才会提交工作。员工可以将工作完成通知放在队列中,但是队列要等到第二天才会发出通知让秘书听到。

感谢 Josh 对纪元的含义以及该术语如何在操作中发挥作用的额外解释。我接受了你的回答,感谢你花时间把它全部写出来。

现在我似乎明白了为什么事件处理程序声明可以在代码中晚于请求的发出,但我仍然不明白为什么我们可以创建一个对象存储然后立即创建一个索引在该对象存储上,而不必等到我们知道对象存储已成功创建,除非它是同步的或在 versionchange 事务/onupgradeneeded 事件中发生了其他特殊情况。 MDN Web Docs 对 createObjectStore 的描述中没有提到任何事件,也没有包含任何侦听器的示例;所以;我只是假设它永远没有必要。

再次感谢。

Why are the requests made before the event handlers are defined?

没关系

For example, the request = objectStore.add(data) is made before the request.onsuccess and request.onerror functions are declared. Is this correct?

是的,它是正确的,因为这也无关紧要。

我会小心你在之前这个词的使用。也许它对我的意义与对你的意义不同。我不知道。但也许这就是让你失望的原因。

Is it possible that the request could complete before the event handlers are registered?

如果您在发出请求时在同一时期注册事件处理程序,则不会。请求仅在稍后的时期完成。


好的,这是我尝试通过示例进行解释的尝试(抱歉,如果这不好!)。拟人化通常是一种很好的教育技术,并且没有使用原始技术术语那么吓人,所以让我们开始吧。

假设您是老板,并且有员工。假设您要求一名员工为您做一些工作。然后,您要求该员工在完成工作后向您的秘书汇报。在要求员工去做其他工作后,您立即继续做自己的工作,而无需等待该员工完成工作并汇报。你们基本上是在同时工作。

现在,在这种情况下,如果您在向员工提出做某事的请求时没有秘书会怎样?嗯,没问题。在该员工完成工作之前,甚至在该员工还不知道向谁汇报之前,您就去雇用另一位秘书,这很好,因为员工只知道他们向您的秘书汇报。员工在被分配工作时不知道您的秘书是否存在,也不需要知道这一点。失踪的秘书并没有阻止该员工开始工作,也没有阻止他理解要完成的工作。当员工完成他们的工作时,您的秘书已经准备好等待。或者,您不这样做,因为您甚至不关心工作是否真正完成,您只是发出命令并相信员工会完成他们的工作,无论如何。如果您需要做一些必须等到第一个项目完成后才能做的其他工作,您真的只关心让他们向您的秘书汇报,这是一个不同的问题。

假设您在为员工分配工作时已经有一位秘书。这种已经有秘书的情况,和你在分配工作后不久,但还没有完成就去雇一个秘书的情况有什么区别?没有区别。

现在,让我们尝试真正解决您的问题。你的意思是,在你知道员工是否完成任务之前,似乎不可能可靠地出去雇用那个秘书。我认为这是严重的误解。这是完全有可能做到这一点的。这是为什么?我想这不是最容易掌握的东西。

我将稍微延伸这个比喻并强加一个奇怪的规则。无论您交给员工的项目多么简单,即使只是 运行 并在早上给您喝咖啡,他们也永远不会在同一天回复您。他们总是会在晚些时候完成工作,最早是明天。他们甚至可能在你告诉他们的一纳秒内完成他们的工作,但他们永远不会马上回复你或你的秘书,他们总是会延迟到最早的明天。

这意味着您有一整天的时间去聘请在您下达命令时不存在的那个秘书。只要你在明天之前完成,你就很好。明天员工回复时,该秘书将存在并为您工作,并且将能够收到员工的消息。


编辑对您添加的评论的回复:

是的,吊装在很多方面都是相似的。事情发生的顺序可能与用代码编写的顺序不同。提升当然是同步的所以不是完全相似,但是乱序方面还是相似的。

Epoch 只是我自己的词,我用它来表示事件循环的单次迭代。就像使用 i for i 从 0 到 2 的 for 循环一样,有 3 个时期,第 0 次迭代,第 1 次迭代和第 2 次迭代。我只是称它们为时期,因为它就像时间的类别。

在承诺的情况下,它甚至可能是微任务。在 js worker 案例中,它可能类似于线程(并且 worker 是旧的 child-iframe 技术的新热点)。基本上这些都是 'realize' 一次做不止一件事的方法。 Node 称它为 tick,并且有诸如 nextTick() 之类的东西将代码执行推迟到其循环的下一个 tick。在一个纪元内,事情按照它们被写入的顺序发生(值得注意的是提升都在纪元 0 中)。但是有些代码可能是异步的,因此会跨时代发生,因此可能 运行 的顺序与编写顺序不同。较早编写的代码可能会出现在较晚的时代。

当你提出要求时,它说,开始做这件事,并在下一个epoch 最早回复我。在当前纪元结束之前,您必须为请求注册处理程序。

有些代码,例如您示例中提到的图像预加载器,必须考虑到它附加侦听器的时间太晚(图像正在备用时间轴中预加载,有些可能已经加载并且在某些浏览器中,这意味着加载不会触发),因此它想要检查 imageElement.complete 以捕获这种情况。在事件侦听器实现的其他情况下,一些调度程序实现将针对已经发生的事件向新添加的侦听器触发事件​​,而新侦听器在事件发生时没有侦听。但这不是事件侦听器实现的普遍特征,只是某些实现的特征。

对于来自 onupgradeneeded 的 transaction.oncomplete 事情,这不是一个很好的例子。它正在做它不需要做的事情。

这是对您问题的技术回答:

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

JS 并发模型与“运行-to-completion”语义协作(不并行处理同一队列中的事件)。这意味着任何异步响应都将作为消息发布到 window 事件循环,并且您在请求后看到的所有顺序代码都保证在异步响应处理开始之前执行。

也就是说,从可用性的角度来看,IndexDB API 并没有以最具表现力的方式提供意图,并且来自其他具有抢占式线程的语言,您可能会感到困惑 :-)