309

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

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

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

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

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

4

5 回答 5

397

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

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

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

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

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

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

这是那篇文章的引述:

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

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

引用 HTML5 规范:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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 模块加载

于 2012-01-25T01:38:51.393 回答
29

@addyosmani的精彩总结

在此处输入图像描述

从https://addyosmani.com/blog/script-priorities/无耻复制

于 2019-02-21T05:05:10.857 回答
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>do something with #blah</script><div id="blah"></div>那么您将收到错误消息。

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

于 2012-01-25T01:42:27.910 回答
9

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

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'])
于 2019-06-17T23:15:10.703 回答
1

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

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

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

我尝试了不同的选项,但是由于上述答案,我了解到如果将 async -attribute 添加到模块类型的脚本中,这意味着脚本会异步加载,但一旦加载它就会立即执行。

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

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

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

摘要:使用

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

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

于 2021-07-18T15:42:34.870 回答