脚本的加载和执行顺序

346

在HTML页面中,包含JavaScript的方式有很多种。我知道以下几个选项:

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

不包括来自硬盘的浏览器脚本、javascript: URIs和onEvent属性[3],已经有16种获取JS执行的选择,我肯定漏掉了某些。

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

由于我担心没有,这是我的具体问题:我有一些(外部)head脚本用于初始化和脚本加载。然后在body末尾有两个静态内联脚本。第一个让脚本加载器动态将另一个脚本元素(引用外部js)附加到body中。第二个静态内联脚本想要使用添加的外部脚本中的js。它能够依赖于其他脚本已经被执行了吗(为什么)?


你看过 Steve Souders 的 无阻塞加载脚本 吗?虽然现在有点过时,但仍然包含了一些有价值的见解,可以帮助理解特定脚本加载技术下浏览器的行为。 - vhs
6个回答

443
如果你没有动态加载脚本或将它们标记为deferasync,那么脚本将按照在页面中遇到的顺序加载。无论是外部脚本还是内联脚本 - 它们都按照在页面中遇到的顺序执行。在外部脚本之后出现的内联脚本会等待所有在它之前的外部脚本加载和运行完毕。
异步脚本(不管它们如何被指定为异步)以不可预测的顺序加载和运行。浏览器并行加载它们,并可以自由地按任意顺序运行它们。
多个异步事物之间没有可预测的顺序。如果需要有可预测的顺序,则必须通过注册异步脚本的加载通知并手动排序javascript调用来实现。
当动态插入脚本标签时,执行顺序的行为取决于浏览器。您可以在this reference article中查看Firefox的行为。简而言之,Firefox的较新版本将动态添加的脚本标签默认设置为异步,除非已经设置了脚本标签。
使用async属性的脚本标签一旦加载就可以运行。实际上,浏览器可能会暂停解析器正在进行的任何其他操作并运行该脚本。因此,它真的可以在几乎任何时间运行。如果脚本被缓存,它可能会立即运行。如果脚本需要较长时间才能加载,则可能在解析器完成后运行。唯一需要记住的是,async可以随时运行,而且时间是不可预测的。
具有defer属性的脚本标记将等待整个解析器完成,然后按照遇到的顺序运行所有标记为defer的脚本。这使您可以将几个相互依赖的脚本标记为defer。它们都将被推迟到文档解析器完成之后运行,但它们将按照它们遇到的顺序执行,保留它们的依赖关系。我认为defer就像将脚本放入队列中,在解析器完成后将进行处理。从技术上讲,浏览器可能会随时在后台下载脚本,但在解析器完成解析页面并解析和运行未标记为defer或async的任何内联脚本之后,它们不会执行或阻塞解析器。

以下是该文章的一句引用:

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

对于较新的兼容浏览器,HTML5规范的相关部分在这里。其中有很多关于异步行为的描述。显然,这个规范不适用于旧浏览器(或非标准浏览器),你可能需要测试它们的行为才能确定。

来自HTML5规范的一句引用:

然后,必须遵循以下第一个描述情况的选项: 如果元素具有src属性,并且元素具有defer属性,并且元素已标记为“parser-inserted”,并且元素没有async属性,则该元素必须添加到将在解析文档完成时执行与创建元素的解析器相关联的脚本列表的末尾。 网络任务源完成提取算法后放置在任务队列上的任务必须设置元素的“准备好执行解析器”标志。解析器将处理执行脚本。 如果元素具有src属性,并且元素已标记为“parser-inserted”,并且元素没有async属性,则该元素是创建元素的解析器的文档的待处理解析阻止脚本。(每个文档一次只能有一个这样的脚本。) 网络任务源完成提取算法后放置在任务队列上的任务必须设置元素的“准备好执行解析器”标志。解析器将处理执行脚本。 如果元素没有src属性,并且元素已标记为“parser-inserted”,并且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”属性设置为true或false来确定脚本如何加载。 - nativist.bill.cutting
defer 和放在 </body> 前的脚本之间有什么区别吗?据我所知,两者似乎在大致相同的时间执行。 - Rudey
@RuudLenders - 使用 defer 标记的脚本会在解析器完成后运行,因此与位于 body 结尾的脚本非常接近。 - jfriend00
1
@RuudLenders - 这取决于浏览器的实现。在文档中较早遇到带有 defer 标记的脚本标签可以让解析器更早地开始下载,同时仍然推迟其执行。请注意,如果您有很多来自同一主机的脚本,则更早地开始下载可能会实际上减慢从同一主机下载其他脚本(因为它们竞争带宽),而您的页面正在等待的其他脚本(不是 defer), 所以这可能是一个双刃剑。 - jfriend00
这篇文章措辞非常得当。异步并行加载依赖脚本和CSS,并控制执行顺序:https://github.com/jhabdas/fetch-inject - vhs
显示剩余13条评论

42

16

浏览器将按照发现脚本的顺序执行它们。如果您调用外部脚本,它将阻止页面直到脚本已加载并执行为止。

为测试此事实:

// 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>对 #blah 进行某些操作</script><div id="blah"></div>),则会出现错误。

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


我可以确认这种行为。但是我们的反馈页面上有提示,它可能只在test.php被缓存时才能正常工作。你知道关于这个的任何规范/参考链接吗? - Bergi
4
link.js没有阻塞。使用类似于您的php脚本的脚本来模拟较长的下载时间。 - 1983
15
这个答案是不正确的。并非总是“动态添加的脚本在附加到文档后立即执行”。有时是这样(例如对于旧版本的Firefox),但通常并非如此。正如jfriend00的答案中所提到的,执行顺序是不确定的。 - Fabio Beltramini
2
脚本按照它们在页面上出现的顺序执行,无论它们是否是内联的,这是没有意义的。那么为什么像Google标签管理器片段和我见过的许多其他片段一样,在页面中插入新脚本的代码会放在所有其他脚本标记之上呢?如果上面的脚本已经被加载,这样做就没有意义了,对吧?还是我漏掉了什么? - RollingInTheDeep

12

经过测试,我发现以下简单解决方案可以在所有现代浏览器中按添加的顺序加载动态加载的脚本。

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次,有时会出现故障。 - MNN TNK
1
“script.async = false” 对我来说确实起作用了,即使在将文件附加到“document.head”时。看起来像这样动态附加时,“async”默认为“true”。谢谢! - Wilt
对我来说有效。我同意@Wilt的观点。 - undefined
我确认@Wilt的猜测是正确的。默认值是true。哦天啊。 - undefined

1
我曾经遇到困难,不知道如何在onload事件发生之前执行嵌入式模块脚本。上面的答案帮了我很多忙,但是我想补充一下我的问题部分解决方案,即“脚本加载和执行顺序”。
我最初使用...引起了一个奇怪的问题,在正常加载页面时它可以运行,但在FireFox调试器中运行时无法工作。这使得调试非常困难。
注意:类型为“module”的脚本始终具有隐含的“deferred”属性,这意味着它们不会停止解析html,也就是说,onload事件可能会在脚本执行之前发生。我不想要那样。但是我确实想使用type="module"来使我的未导出的JavaScript函数和变量对同一页上的其他脚本不可见。
我尝试了不同的选项,但幸亏有了上面的答案,我意识到如果你给一个类型为module的脚本添加async属性,这意味着该脚本异步加载,但一旦加载完成,它就会立即执行。
但在我的情况下,这是一个嵌入在HTML页面中的脚本。因此,它已经随着页面一起加载,根本不需要“异步”加载。由于这个变化,它立即得到了执行——这正是我想要的。

因为这个案例有些反直觉,所以我认为指出这个特定的情况是值得的:要立即执行嵌入脚本,必须将ASYNC属性添加到它的标签中。

通常人们可能会认为“async”意味着某些事情异步发生,按不确定顺序进行,而不是立即执行。但需要注意的是,“async”引起异步加载,但在加载完成后立即执行。当脚本被嵌入时,不需要加载,因此可以立即执行。

总结:使用

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

将一个模块脚本嵌入到HTML页面中以立即执行。

2
一开始似乎这样做是有效的,但如果所有其他的JavaScript文件都已经被缓存,有时它就不起作用了,最终会在列表底部运行。 - Brain2000

-1

完美匹配您的查询!

如果没有任何解决方案适用于您,请参考我开发的以下解决方案。

我也在寻找解决方案,但是经过大量搜索后,我总结了下面的代码,它对我来说完美地工作!

当您希望实现这样的功能时,此代码非常有用:仅在前一个脚本完全加载后才加载下一个脚本!

只需创建一个名为jsLoadScripts.js的文件,并将其插入到头部或正文底部即可。

//From Shree Aum Software Solutions
//aumsoftwaresolutions@gmail.com

//script incrementor for array
scriptIncrementor = 0;

//define your script urls here
let scripts = [
    "https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js",
    "jsGlobalFunctions.js",
    "jsDateParser.js",
    "jsErrorLogger.js",
    "jsGlobalVariables.js",
    "jsAjaxCalls.js",
    "jsFieldsValidator.js",
    "jsTableClickEvent.js",
    "index.js",
    "jsOnDocumentReady.js",
];

//it starts with the first script and then adds event listener to it. which will load another script on load of it. then this chain goes on and on by adding dynamic event listeners to the next scripts! 
function initializeScripts() {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = scripts[scriptIncrementor];
    document.head.appendChild(script);
    script.addEventListener("load", function () {
        loadNextScript();
        scriptIncrementor++;
    });
}

// this function adds event listener to the scripts passed to it and does not allow next script load until previous one has been loaded!
function loadNextScript() {
    if (scriptIncrementor != scripts.length - 1) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        script.src = scripts[scriptIncrementor + 1];
        document.head.appendChild(script);
        script.addEventListener("load", function () {
            loadNextScript();
            scriptIncrementor++;
        });
    }
}

// start fetching your scripts
window.onload = function () {
    initializeScripts();
};

这可能会导致一些速度相关的问题,所以你可以根据自己的需求调用函数initializeScripts()


这是一种不错的技术(但缺乏错误处理,并且非常缓慢,因为采用了顺序加载),但它根本没有回答脚本加载的工作原理。 - Bergi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接