问题 异步Javascript程序如何交互


回到网络开发的早期阶段,我找到了一些民间智慧,对于像这样的代码

<script src=".../program1.js"></script>
<script src=".../program2.js"></script>

浏览器会暂停,加载javascript,编译,执行,继续下一步 script 标记,并重复。通过这种方式,浏览器将在页面上的所有javascript中运行并将其视为一个线性程序。

然而,在勇敢的新现代javascript世界中,我们通过它进行异步加载 async 属性

<script src=".../program1.js" async></script>
<script src=".../program2.js" async></script>

我的理解是这是一个 好事 因为现在浏览器不需要暂停,下载脚本并执行它。相反,它开始下载脚本,但将继续解析DOM。即等待javascript下载时网页不再阻塞。 (如果这不是真的,我会很感激修正)。

然而,更不清楚(并且更难测试)是这两个程序如何相互作用。它们似乎在相同的共享空间中运行(即,从用户角度来看,javascript仍然是单线程的,具有两个(全局,函数)范围)。但是,在我阅读的文档中,它们执行的顺序似乎不明确。

我已经阅读了MDN的文章 并发模型和事件循环。虽然有趣且有用,但它并没有完全回答我的问题。从我收集的内容,当浏览器加载时 program1.js 要么 program2.js,javascript将向事件队列添加消息,并且当javascript引擎在事件循环中运行时将处理该消息。

对我来说缺少的是 - 这个消息说的是什么?是每个程序的单个消息,说“编译并执行所有这些javascript代码”?或者每个程序创建多个消息 - 在我看来可能看起来像

  • 消息1:从该程序中提取所有函数并进行编译
  • 消息2:从此程序中提取全局范围内的所有语句和表达式
  • 消息3-n:将每个语句和表达式添加为单独的队列消息,以供以后处理。

当浏览器处于处理过程中会发生什么 program1.js,但完成下载 program2.js?是否可能交错执行每个程序的语句?

我意识到,作为客户端开发人员,这里的最佳实践是不依赖于全局范围并编写每个程序和函数,因此无论如何调用它都无关紧要,并且不会阻止其他人的代码。但是,我花了很多时间处理其他人的代码,其中一些代码表现不佳。我想了解幕后发生的事情,或者这是否是未定义的行为,与引擎无关,并且不会在实现之间排列。


10800
2017-12-15 17:34


起源

来自 规范: “以依赖于实现的方式,获取ECMAScript源文本(参见第10节)以及零个或多个ECMAScript脚本和/或ECMAScript模块的任何相关主机定义值。对于每个这样的sourceText和hostDefined,如果sourceText是源代码一个脚本,然后执行EnqueueJob(“ScriptJobs”,ScriptEvaluationJob,«sourceText,hostDefined»)。[...]“ - Felix Kling
谢谢菲利克斯! +1是有用的信息 - 但是,由于我没有深入实施javascript引擎,所以上述规范如何转化为实际的javascript行为并不是100%清楚。即懒洋洋地阅读你的有用评论,我想知道是否有一个ScriptEvaluationJob最终为每个加载的javascript文件排队附加消息。 - Alan Storm
如果你搜索 EnqueueJob 在规范中你会看到唯一的其他情况 EnqueueJob 被称为是在承诺的背景下。所以你的问题的答案是:是的,更多的工作可以入队。但是,我必须承认,我不清楚如何 浏览器的  事件循环 与此有关。 - Felix Kling


答案:


有两篇文章说明了实际意义上的“异步”和“延迟”属性(我在浏览器内部是绿色的):

从2014年开始,优秀+简单的图形: http://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html

从2016年开始,为什么异步可能是一种反模式: http://calendar.perfplanet.com/2016/prefer-defer-over-async/

异步

对于您的问题,“异步”指示浏览器执行的操作是:

  1. 尽早开始下载此资源。
  2. 继续,并可以并行下载其他资源。
  3. 但是一旦我的下载完成,就停止渲染并尽早开始执行我。

有效地“异步”仍然可以阻止渲染和后续执行,但它允许解析器继续工作直到执行开始。

多个异步脚本无法保证它们将执行的顺序。取决于加载的速度。这使得像RequireJS这样的AMD系统可以定义依赖关系和回调来加载资源异步但排队执行,直到“全局”环境包含所有先决条件,并且执行顺序可以通过rune magic协商(希望如此)。

推迟

Defer表现得像这样:

  1. 尽早开始下载此资源。
  2. 继续,并可以并行下载其他资源。
  3. 但是一旦我的下载完成,在解析器完成之前什么都不做。
  4. 现在按照发现的顺序执行所有脚本。

一方面,“延迟”更快,因为它永远不会阻止解析器或渲染。但是“延迟”可能会更慢,因为它必须等待执行直到管道清除。

这听起来像“异步”总是更好,但如果你在弱CPU手机上加载2 MB的JS与快速连接,你可能最终等待10s执行,然后才允许解析器完成渲染。使用“延迟”可以防止您的交互层被延迟。

如果您正在谈论客户端/服务器或客户端应用程序,那么区别就更模糊了。在像Magento这样的后端重型应用程序中使用延迟可能更有利,其中渲染是在服务器端处理的。

在一个完全客户端的应用程序中,你可能会得到零内容,直到JS monolith被加载,所以“defer”并没有真正为你做任何事情,但是如果你的整个应用程序是一个庞大的JS包,那么“async”也没有无所事事。


7
2017-12-15 18:11



并且对于不解决“并发和事件循环”而道歉。我知道你在寻找化学工程师对摩擦改性剂的看法,我给出了使用合成油或常规油的机械观点(它们都比用水换油更好)。 - Brendan Falkowski
这两个视角都很有用@Brendan - 我实际上正在寻找一个从上到下解释它的统一字符串理论:) - Alan Storm


是否可能交错执行每个程序的语句?

不,绝对不是。 JS仍然是单线程的,一个程序在另一个程序之后运行(尽管其中哪个程序首先可能是未知的)。

该事件循环消息说什么?

消息是 ScriptEvaluationJob。假设解析脚本成功,它将在一次运行中实例化所有声明并评估脚本体。


4
2017-12-15 18:04