【问题标题】:Why are indexedDB requests made before the event handlers are declared?为什么在声明事件处理程序之前发出 indexedDB 请求?
【发布时间】:2018-07-18 05:48:42
【问题描述】:

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

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

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

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

谢谢。

重复响应

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

但是,在再次阅读答案后,除了接受它并希望它永远有效之外,我还没有进一步了解。我不是要冷嘲热讽。从某种意义上说,我对事件循环和纪元的思考能力有限,而且所有事情都是同时发生的,这让我感到困惑。

在 epoch 结束(或下一个开始,无论您认为更容易理解什么),底层 JS 引擎返回并查看注册执行的内容,然后几乎同时执行所有内容。

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

在这篇由 Jake Archibald 撰写的关于 Promise 的 article 中,他在介绍中展示了一个关于图像加载的示例并写道:

不幸的是,在上面的示例中,事件可能在我们开始监听它们之前就发生了,因此我们需要使用图像的“完整”属性来解决这个问题。

和

这不会捕获在我们有机会收听之前出错的图像;不幸的是,DOM 并没有给我们一种方法来做到这一点。此外,这是加载一个图像,如果我们想知道一组图像何时加载,事情会变得更加复杂。

这给人的印象是顺序很重要,因此,在图像的情况下,如果可能,应在声明所有事件处理程序后分配源,以免错过听力事件。对我来说重要的部分是事件可以在事件处理程序被声明/注册之前发生。

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

即使所有语句都是异步的,例如 MDN Web Docs on Using 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);
    });
  };
};

对答案/评论的澄清/回应

感谢您抽出宝贵时间回答我的问题并提供补充说明。

首先,“之前”是指语句在脚本中出现的顺序。

我认为我遵循您的类比,这是一个很好的类比。我只是仍然不清楚为什么员工直到第二天才能将他们的工作提交给秘书,因为可以保证秘书会在那里接收它。

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

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

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

但是,我仍然想更好地理解什么是纪元,至少在知道声明是在同一个纪元内作出的方面。我在 MDN Web Docs 中关于“并发模型和事件循环”的文章中没有看到任何时代的提及。您介意向我指出您知道的任何好的资源吗?

谢谢。

最后说明

我通过一个链接发现了这两个项目,这里是堆栈溢出。八年前提出了同样的问题,并以相同的方式回答了不同的术语。也就是说,javascript 代码将“运行至完成”或具有运行至完成语义,而不是 epoch。这个question 向您推荐这个document,可以搜索“运行到完成”来阅读两个关于为什么在注册事件处理程序之前发出请求的设置中没有竞争条件的交流。 David Flanagan 写的较早的 JavaScript 书在讨论 JS“程序”的执行时指出,由于 JS 具有单线程执行,因此不必担心竞争条件。但我不知道他是不是指的正是这种情况。

因此,这个问题在过去已经被多次询问和回答,我想我只是另一个新手问一个老问题,就好像我是第一个想到它的人一样,并且对 JS 的处理方式没有足够的了解.

上面链接的文章“并发模型和事件循环”有一个简短的“运行到完成”部分;但直到阅读了上面链接的最后一个文档后,我才明白它的含义。

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

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

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

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

感谢 Josh 对时代的含义以及该术语如何在操作中发挥作用的额外解释。我接受了您的回答,并感谢您抽出时间将其全部写出来。

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

再次感谢。

【问题讨论】:

  • 我不确定要在我以前的答案中添加什么。也许试着问一个更狭窄、更清晰、更具体的问题?我将是第一个承认从编写同步代码到异步代码的飞跃确实令人困惑的人。
  • 平心而论,img 的例子是一个不幸的例子。 Img onload 也应该异步触发;即即使您确实设置了 onload 处理程序,但您确实设置了它的 src,但仍然以同步方式,您的事件应该触发。这是某些实现中的错误,可能是因为可以通过多种方式设置此 src(例如直接从标记中)。

标签: javascript indexeddb


【解决方案1】:

为什么在定义事件处理程序之前发出请求?

没关系。

例如,request = objectStore.add(data) 在 request.onsuccess 和 request.onerror 函数声明之前创建。它是否正确?

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

我会小心你使用这个词之前。也许它对我的意义与对你不同。我说不出来。但也许这就是让你绊倒的原因。

请求是否有可能在事件处理程序注册之前完成?

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


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

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

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

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

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

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

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


编辑对您添加的 cmets 的响应:

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

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

在 Promise 的情况下,它甚至可能是一个微任务。在 js worker 案例中,它可能是类似线程的(并且 worker 是旧的 child-iframe 技术的新热点)。基本上,这些都是“意识到”一次做不止一件事的方法。 Node 将其称为一个滴答声,并具有类似nextTick() 的东西,它会将代码执行推迟到其循环的下一个滴答声。在单个 epoch 中,事情按照它们被写入的顺序发生(特别是提升都在 epoch 0 中)。但是有些代码可能是异步的,因此会跨时代发生,因此可能会以与编写时不同的顺序运行。较早编写的代码可能会在较晚的时期发生。

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

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

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

【讨论】:

  • 这是一个非常复杂的解释,IMM 并没有解决 OP 的担忧。为了扩展这个比喻,您的员工不会等待任何事情。你有一篮子已完成的任务。您的秘书必须在每天工作结束时处理这个篮子的内容。这样,无论您的员工处理任务的速度有多快,他们的结果都是在篮子里等待秘书处理。任务何时下达也没关系,你的秘书是否已经知道如何处理这些任务,只要他们在一天工作结束之前就知道。
  • @Josh 谢谢你的解释。我在问题的最后写了一个简短的回复。
  • @Kaiido谢谢你的cmets。直到现在我才明白这一点。您对隐喻的扩展是否准确意味着异步请求(员工的任务)可能在事件处理程序注册(雇用秘书)之前完成,但通知将保留在队列中(完成任务的篮子)并且在注册事件处理程序并且函数运行完成(工作日结束)之前不会执行(通知秘书)?
  • @Gary 很高兴我能帮上忙
  • @Gary 是的,有可能,假设异步任务完全由 js 之外的其他进程执行。下面是一个使用 web worker 的例子:jsfiddle.net/xg08cjys
【解决方案2】:

这是您问题的技术答案:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 2019-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-10
    相关资源
    最近更新 更多