加载和执行脚本的顺序

IT技术 javascript load-order
2021-01-09 03:15:48

在 html 页面中包含 JavaScript 的方法有很多种。我知道以下选项:

  • 内联代码或从外部 URI 加载
  • 包含在 <head> 或 <body> 标签中 [ 1 , 2 ]
  • 没有deferasync属性(只有外部脚本)
  • 包含在静态源中或由其他脚本动态添加(在不同的解析状态下,使用不同的方法)

不算来自硬盘的浏览器脚本、javascript:URIs 和onEvent-attributes [ 3 ],已经有 16 种替代方法可以让 JS 执行,而且我确定我忘记了一些东西。

我不太关心快速(并行)加载,我对执行顺序更好奇(这可能取决于加载顺序和文档顺序)。是否有涵盖所有情况的良好(跨浏览器)参考?例如http://www.websiteoptimization.com/speed/tweak/defer/只处理其中的 6 个,并且主要测试旧浏览器。

我担心没有,这是我的具体问题:我有一些(外部)头脚本用于初始化和脚本加载。然后我在正文的末尾有两个静态的内联脚本。第一个让脚本加载器动态地将另一个脚本元素(引用外部 js)附加到正文中。第二个静态内联脚本想要使用来自添加的外部脚本的 js。它可以依赖另一个已被执行(以及为什么:-)?

5个回答

如果您没有动态加载脚本或将它们标记为deferasync,则脚本将按照页面中遇到的顺序加载。无论是外部脚本还是内联脚本都无关紧要 - 它们按照在页面中遇到的顺序执行。在外部脚本之后的内联脚本被保留,直到它们之前的所有外部脚本都加载并运行。

异步脚本(无论它们如何指定为异步)以不可预测的顺序加载和运行。浏览器并行加载它们,并且可以按照它想要的任何顺序自由运行它们。

多个异步事物之间没有可预测的顺序。如果需要一个可预测的顺序,则必须通过从异步脚本注册加载通知并在加载适当的内容时手动对 javascript 调用进行排序来对其进行编码。

当动态插入脚本标签时,执行顺序的行为将取决于浏览器。您可以在这篇参考文章中了解 Firefox 的行为简而言之,除非另外设置了脚本标签,否则较新版本的 Firefox 默认将动态添加的脚本标签设为异步。

一个脚本标签async可以在加载后立即运行。事实上,浏览器可能会暂停解析器正在执行的任何其他操作并运行该脚本。所以,它几乎可以在任何时候运行。如果脚本被缓存,它可能几乎立即运行。如果脚本需要一段时间加载,它可能会在解析器完成后运行。要记住的一件事async是它可以随时运行,而且时间是不可预测的。

使用脚本标签defer等待,直到整个解析器完成,然后运行标有所有脚本defer的顺序,他们遇到。这允许您将多个相互依赖的脚本标记为defer. 它们都将被推迟到文档解析器完成之后,但它们将按照遇到的顺序执行,并保留它们的依赖关系。我认为defer脚本被放入一个队列,在解析器完成后将被处理。从技术上讲,浏览器可能会随时在后台下载脚本,但在解析器完成页面解析并解析和运行任何未标记defer或 的内联脚本之前,它们不会执行或阻止解析器async

这是那篇文章的引述:

插入脚本的脚本在 IE 和 WebKit 中异步执行,但在 Opera 和 4.0 之前的 Firefox 中同步执行。

HTML5 规范的相关部分(适用于较新的兼容浏览器)在这里那里有很多关于异步行为的文章。显然,此规范不适用于您可能必须测试以确定其行为的旧浏览器(或不符合标准的浏览器)。

引用 HTML5 规范:

然后,必须遵循以下描述情况的选项中的第一个:

如果该元素有 src 属性,并且该元素有 defer 属性,并且该元素已被标记为“解析器插入”,并且该元素没有 async 属性 该元素必须添加到列表的末尾当文档完成与创建元素的解析器的文档相关联的解析时将执行的脚本。

一旦获取算法完成,网络任务源放置在任务队列上的任务必须设置元素的“准备好被解析器执行”标志。解析器将处理执行脚本。

如果该元素具有 src 属性,并且该元素已被标记为“解析器插入”,并且该元素没有 async 属性 该元素是创建该元素的解析器的 Document 的挂起解析阻止脚本。(每个文档一次只能有一个这样的脚本。)

一旦获取算法完成,网络任务源放置在任务队列上的任务必须设置元素的“准备好被解析器执行”标志。解析器将处理执行脚本。

如果该元素没有 src 属性,并且该元素已被标记为“已插入解析器”,并且创建脚本元素的 HTML 解析器或 XML 解析器的 Document 具有阻止脚本样式表,则该元素是创建元素的解析器的文档的暂挂解析阻止脚本。(每个文档一次只能有一个这样的脚本。)

设置元素的“准备好被解析器执行”标志。解析器将处理执行脚本。

如果元素有 src 属性,没有 async 属性,并且没有设置“force-async”标志元素必须添加到将尽快执行的脚本列表的末尾关联在准备脚本算法开始时使用脚本元素的文档。

一旦获取算法完成,网络任务源放置在任务队列中的任务必须运行以下步骤:

如果该元素现在不是脚本列表中将尽快按顺序执行的第一个元素(如上面添加的那样),则将该元素标记为就绪,但在不执行脚本的情况下中止这些步骤。

执行:执行此脚本列表中第一个脚本元素对应的脚本块,将尽快按顺序执行。

从此脚本列表中删除将尽快按顺序执行的第一个元素。

如果这个将尽快按顺序执行的脚本列表仍然不为空并且第一个条目已经标记为就绪,则跳回标记为执行的步骤。

如果元素具有 src 属性,则必须将元素添加到脚本集,该脚本集将在准备脚本算法开始时尽快执行脚本元素的文档。

一旦获取算法完成,网络任务源放置在任务队列中的任务必须执行脚本块,然后从将尽快执行的脚本集中移除元素。

否则用户代理必须立即执行脚本块,即使其他脚本已经在执行。


Javascript module脚本type="module"呢?

Javascript 现在支持module加载,语法如下:

<script type="module">
  import {addTextToBody} from './utils.mjs';

  addTextToBody('Modules are pretty cool.');
</script>

或者,使用src属性:

<script type="module" src="http://somedomain.com/somescript.mjs">
</script>

所有具有 的脚本都会type="module"自动赋予该defer属性。这会与页面的其他加载并行(如果不是内联)下载它们,然后按顺序运行它们,但在解析器完成之后。

module脚本也可以被赋予一个async属性,属性将尽快运行内联module脚本,而不是等到解析器完成,也不等待以async相对于其他脚本的任何特定顺序运行脚本。

有一个非常有用的时间线图表,显示了不同脚本组合的获取和执行,包括本文中的module脚本:Javascript module加载

感谢您的回答,但问题是脚本动态添加到页面的,这意味着它被认为是 async还是只在 <head> 中有效?我的经验也是它们是按文档顺序执行的?
2021-03-13 03:15:48
@Bergi - 如果它是动态添加的,那么它是异步的并且执行顺序是不确定的,除非您编写代码来控制它。
2021-03-16 03:15:48
@Bergi - 好的,我已经修改了我的答案,说异步脚本以不确定的顺序加载。它们可以按任何顺序加载。如果我是你,我不会指望 Kolink 的观察总是如此。我知道没有任何标准规定动态添加的脚本必须立即运行并且必须阻止其他脚本运行直到它被加载。我希望这取决于浏览器,也可能取决于环境因素(脚本是否被缓存等)。
2021-03-21 03:15:48
只是,Kolink 的说法正好相反……
2021-03-27 03:15:48
@RuudLenders - 这取决于浏览器的实现。遇到文档中较早的脚本标记,但标记defer为 使解析器有机会更快地开始下载,同时仍然推迟其执行。请注意,如果您有很多来自同一主机的脚本,那么早点开始下载实际上可能会减慢从您的页面正在等待的(不是defer同一主机(因为它们争夺带宽)下载其他脚本的速度,因此这可能是一把双刃剑。
2021-04-07 03:15:48

浏览器将按照它找到的顺序执行脚本。如果你调用一个外部脚本,它会阻塞页面,直到脚本被加载和执行。

要测试这个事实:

// file: test.php
sleep(10);
die("alert('Done!');");

// HTML file:
<script type="text/javascript" src="test.php"></script>

动态添加的脚本会在附加到文档后立即执行。

要测试这个事实:

<!DOCTYPE HTML>
<html>
<head>
    <title>Test</title>
</head>
<body>
    <script type="text/javascript">
        var s = document.createElement('script');
        s.type = "text/javascript";
        s.src = "link.js"; // file contains alert("hello!");
        document.body.appendChild(s);
        alert("appended");
    </script>
    <script type="text/javascript">
        alert("final");
    </script>
</body>
</html>

警报顺序是“附加”->“你好!” - >“最终”

如果在脚本中您尝试访问尚未访问的元素(例如:),<script>do something with #blah</script><div id="blah"></div>那么您将收到错误消息。

总的来说,是的,您可以包含外部脚本,然后访问它们的函数和变量,但前提是您退出当前<script>标签并开始一个新标签。

link.js 没有阻塞。使用类似于您的 php 的脚本来模拟较长的下载时间。
2021-03-16 03:15:48
这个答案是不正确的。“动态添加的脚本在附加到文档后立即执行”并非总是如此。有时这是真的(例如对于旧版本的 Firefox),但通常不是。jfriend00 的回答中提到的执行顺序是不确定的。
2021-04-06 03:15:48
无论脚本是否内联,都按照它们出现在页面上的顺序执行脚本是没有意义的。那么为什么 Google 标签管理器片段和我见过的许多其他片段会有代码在页面中的所有其他脚本标签之上插入一个新脚本?如果上面的脚本肯定已经加载了,那么这样做是没有意义的??或者我错过了什么。
2021-04-07 03:15:48
我可以确认这种行为。但是我们的反馈页面上有提示,它可能仅在缓存 test.php 时才有效。您知道有关此的任何规范/参考链接吗?
2021-04-08 03:15:48

在测试了许多选项后,我发现以下简单的解决方案是按照在所有现代浏览器中添加的顺序加载动态加载的脚本

loadScripts(sources) {
    sources.forEach(src => {
        var script = document.createElement('script');
        script.src = src;
        script.async = false; //<-- the important part
        document.body.appendChild( script ); //<-- make sure to append to body instead of head 
    });
}

loadScripts(['/scr/script1.js','src/script2.js'])
不连续工作,如果你尝试 10 次,有时它会出现故障
2021-03-22 03:15:48

我无法理解如何在 onload 事件发生之前执行嵌入式module脚本。上面的答案有很大帮助,但让我添加部分答案,说明是什么解决了我对“加载和执行脚本顺序”的误解的特定问题。

我第一次使用 ... 这导致了一个奇怪的问题,它在正常加载页面时起作用,但在 FireFox 上的调试器中运行时不起作用。这使得调试变得非常困难。

注意:类型为“module”的脚本总是有一个隐含的“deferred”属性,这意味着它们不会停止解析 html,这意味着 onload-event 可以在脚本执行之前发生。我不想那样。但我确实想使用 type="module" 使我未导出的 JavaScript 函数和变量对同一页面上的其他脚本不可见。

我尝试了不同的选项,但由于上述答案,我了解到如果将 async 属性添加到类型为 module 的脚本中,则意味着该脚本会异步加载,但一旦加载它就会立即执行。

但就我而言,这是嵌入在 HTML 页面中的脚本。因此,它意味着不需要“异步”加载。它已经与页面一起加载,因为它已嵌入其中。因此,带有此更改的它确实会立即执行——这正是我想要的。

所以我认为有必要指出这种特殊情况,因为它有点违反直觉:要立即执行嵌入的脚本,您必须将 ASYNC 属性添加到其标记中。

通常人们可能认为“异步”意味着某些事情以不确定的顺序异步发生,而不是立即发生。但要意识到的是,“async”导致异步加载,但加载完成后立即执行。嵌入脚本后,无需加载,因此您可以立即执行。

总结:使用

     <script type="module" async> ... </script>

获取嵌入到 HTML 页面的module脚本以立即执行。