加载和执行脚本的顺序


265

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

  • 内联代码或从外部URI加载
  • 包括在<HEAD>或<body>标记[ 12 ]
  • 没有属性deferasync属性(仅外部脚本)
  • 包含在静态源中或由其他脚本动态添加(处于不同的解析状态,具有不同的方法)

不计算硬盘中的浏览器脚本,javascript:URI和onEvent-attributes [ 3 ],已经有16种方法可以执行JS,我敢肯定我忘了一些东西。

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

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


您是否看过史蒂夫·苏德斯(Steve Souders)加载脚本而不阻塞的情况?现在有些过时了,但是在使用特定脚本加载技术的情况下,仍然包含一些对浏览器行为的宝贵见解。
乔什·哈布达斯

Answers:


331

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

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

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

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

带有的脚本标签async可以在加载后立即运行。实际上,浏览器可能会暂停解析器的运行,并运行该脚本。因此,它几乎可以随时运行。如果脚本被缓存,它可能几乎立即运行。如果脚本需要一段时间才能加载,则它可能在解析器完成后运行。要记住的一件事async是它可以随时运行,并且时间是不可预测的。

带有脚本标记的脚本要defer等到整个解析器完成后,再按defer遇到的顺序运行所有标有的脚本。这样,您就可以将几个相互依赖的脚本标记为defer。它们都将被推迟到文档解析器完成之后,但是它们将按照保留它们的依赖关系的顺序执行。我认为defer脚本已放入解析器完成处理的队列中。从技术上讲,浏览器可能会随时在后台下载脚本,但是直到解析器解析完页面并解析并运行任何未标记defer或的内联脚本后,它们才会执行或阻止解析器async

这是该文章的引文:

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

HTML5规范的相关部分(适用于较新的兼容浏览器)在此处。那里有很多关于异步行为的文章。显然,此规范不适用于您可能需要测试以确定其行为的旧版浏览器(或格式不正确的浏览器)。

HTML5规范的引文:

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

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

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

如果该元素具有src属性,并且该元素已被标记为“插入分析器”,并且该元素没有异步属性,则 该元素是创建该元素的分析器的文档的待处理分析阻止脚本。(每个文档一次只能有一个这样的脚本。)

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

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

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

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

获取算法完成后,网络任务源将其放置在任务队列上的任务必须运行以下步骤:

如果该元素现在不是脚本列表中第一个要按顺序执行的元素那么该元素将被添加到上面,请将该元素标记为就绪,但在不执行脚本的情况下放弃这些步骤。

执行:执行与该脚本列表中第一个脚本元素相对应的脚本块,这些脚本块将尽快执行。

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

如果此将尽快执行的脚本列表仍然不为空,并且第一项已被标记为就绪,则跳回到标记为执行的步骤。

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

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

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


那么Javascript模块脚本type="module"呢?

Javascript现在支持使用以下语法加载模块:

<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属性。这将与页面的其他加载并行(如果不是内联)下载它们,然后按顺序运行它们,但在解析器完成之后。

还可以为模块脚本赋予async属性,该属性将尽快运行内联模块脚本,而不是等到解析器完成后才开始,并且不等待async相对于其他脚本以任何特定顺序运行脚本。

有一个非常有用的时间表,显示了脚本的不同组合的获取和执行,包括本文中的模块脚本:Javascript Module Loading


感谢您的回答,但是问题是脚本动态添加到页面的,这意味着它被认为是async。还是仅在<head>中起作用?我的经验还在于它们是按文档顺序执行的?
Bergi 2012年

@Bergi-如果它是动态添加的,则它是异步的,并且执行顺序不确定,除非您编写代码来控制它。
jfriend00 2012年

只是,科林克(Kolink)提出了相反的意见
Bergi 2012年

@Bergi-好的,我已经修改了答案,说异步脚本以不确定的顺序加载。它们可以以任何顺序加载。如果我是你,我不会指望Kolink的观察一直如此。我不知道有任何标准说动态添加的脚本必须立即运行,并且必须阻止其他脚本运行,直到加载完毕。我希望这取决于浏览器,也可能取决于环境因素(是否缓存脚本等)。
jfriend00 2012年

1
@RuudLenders-这取决于浏览器的实现。在文档的前面遇到script标记,但标记为,defer则使解析器有机会尽早开始下载,同时仍推迟执行。请注意,如果您有许多脚本来自同一主机,那么尽早开始下载可能实际上会减慢您的页面正在等待的其他主机(因为它们竞争带宽)的下载(不是defer),因此这可能是一把双刃剑。
jfriend00

13

浏览器将按找到脚本的顺序执行脚本。如果调用外部脚本,它将阻塞页面,直到脚本被加载并执行为止。

要测试这个事实:

// 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>标记并开始一个新的标记。


我可以确认这种行为。但是在我们的反馈页面上有一些提示,它可能仅在test.php被缓存时才起作用。您知道与此有关的任何规格/参考链接吗?
Bergi 2012年

4
link.js没有被阻止。使用类似于您的php的脚本来模拟较长的下载时间。
1983年

14
这个答案是不正确的。“动态添加的脚本一被添加到文档中就立即执行”并不总是这种情况。有时这是正确的(例如,对于旧版本的Firefox),但通常并非如此。如jfriend00的答案所述,执行顺序是不确定的。
Fabio Beltramini 2014年

1
无论脚本是否内联,按它们在页面上出现的顺序执行都是没有意义的。那么,为什么Google标记管理器代码段以及我见过的许多其他代码都具有在页面中所有其他脚本标记上方插入新脚本的代码?如果确实已经加载了上述脚本,那么这样做就没有意义了?还是我错过了一些东西。
user3094826


2

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

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'])
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.