在 html 页面中包含 JavaScript 有很多不同的方法。我知道以下选项:
内联代码或从外部 URI 加载
包含在
或 标记中 [1,2]没有、延迟或异步属性(仅外部脚本)
包含在静态源中或由其他脚本动态添加(在不同的解析状态下,使用不同的方法)
不计算硬盘中的浏览器脚本、javascript:URI 和 onEvent
-attributes [3],已经有 16 种替代方法可以让 JS 执行,我确定我忘记了一些东西。
我不太关心快速(并行)加载,我对执行顺序更好奇(这可能取决于加载顺序和document order)。 是否有一个很好的(跨浏览器)参考可以涵盖所有情况?例如 http://www.websiteoptimization.com/speed/tweak/defer/ 只处理其中的 6 个,并且主要测试旧浏览器。
我担心没有,这是我的具体问题:我有一些(外部)头脚本用于初始化和脚本加载。然后我在正文的末尾有两个静态的内联脚本。第一个允许脚本加载器动态地将另一个脚本元素(引用外部 js)附加到正文。第二个静态内联脚本想要使用添加的外部脚本中的 js。它可以依赖于另一个已被执行(以及为什么:-)?
如果您没有动态加载脚本或将它们标记为 defer
或 async
,则脚本会按照页面中遇到的顺序加载。无论是外部脚本还是内联脚本都没有关系 - 它们按照在页面中遇到的顺序执行。外部脚本之后的内联脚本会一直保留,直到它们之前的所有外部脚本都已加载并运行。
异步脚本(无论它们如何指定为异步)以不可预知的顺序加载和运行。浏览器并行加载它们,并且可以按照它想要的任何顺序自由运行它们。
多个异步事物之间没有可预测的顺序。如果需要一个可预测的顺序,则必须通过注册来自异步脚本的加载通知并在加载适当的内容时手动排序 javascript 调用来对其进行编码。
当动态插入脚本标签时,执行顺序的行为将取决于浏览器。您可以在 this reference article 中查看 Firefox 的行为。简而言之,新版本的 Firefox 默认将动态添加的脚本标签设置为异步,除非另外设置了脚本标签。
带有 async
的脚本标签可以在加载后立即运行。实际上,浏览器可能会暂停解析器正在执行的任何其他操作并运行该脚本。因此,它几乎可以随时运行。如果脚本被缓存,它可能几乎立即运行。如果脚本需要一段时间才能加载,它可能会在解析器完成后运行。 async
要记住的一件事是它可以随时运行,而那个时间是不可预测的。
带有 defer
的脚本标记等待整个解析器完成,然后按照遇到的顺序运行所有标有 defer
的脚本。这允许您将多个相互依赖的脚本标记为 defer
。它们都将被推迟到文档解析器完成之后,但它们将按照遇到的顺序执行,保留它们的依赖关系。我认为 defer
就像脚本被放入将在解析器完成后处理的队列中。从技术上讲,浏览器可能随时在后台下载脚本,但它们不会执行或阻止解析器,直到解析器完成页面解析并解析和运行任何未标记为 defer
的内联脚本或async
。
这是那篇文章的引述:
插入脚本的脚本在 IE 和 WebKit 中异步执行,但在 Opera 和 4.0 之前的 Firefox 中同步执行。
HTML5 规范(适用于较新的兼容浏览器)的相关部分是 here。那里有很多关于异步行为的文章。显然,此规范不适用于您可能需要测试才能确定其行为的旧浏览器(或不符合标准的浏览器)。
引用 HTML5 规范:
然后,必须遵循以下描述情况的选项中的第一个:如果元素具有 src 属性,并且元素具有 defer 属性,并且该元素已被标记为“解析器插入”,并且该元素没有具有异步属性该元素必须添加到脚本列表的末尾,当文档完成与创建该元素的解析器的文档相关联的解析时将执行该脚本。一旦获取算法完成,网络任务源放置在任务队列中的任务必须设置元素的“准备好被解析器执行”标志。解析器将处理执行脚本。如果该元素具有 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 Module Loading。
@addyosmani 的精彩总结
https://i.stack.imgur.com/lHuHk.png
无耻地从https://addyosmani.com/blog/script-priorities/复制
浏览器将按照找到它们的顺序执行脚本。如果您调用外部脚本,它将阻塞页面,直到脚本被加载并执行。
为了测试这个事实:
// 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>
标记并开始一个新的。
在测试了许多选项后,我发现以下简单的解决方案是按照它们在所有现代浏览器中添加的顺序加载动态加载的脚本
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'])
我很难理解如何在 onload 事件发生之前执行嵌入式模块脚本。上面的答案有很大帮助,但让我添加一个部分答案,说明什么解决了我误解“脚本的加载和执行顺序”的特殊问题。
我第一次使用 ... 这导致了一个奇怪的问题,它在正常加载页面时工作,但在 FireFox 的调试器中运行时却没有。这使得调试非常困难。
注意:类型为“module”的脚本总是有一个隐含的“deferred”属性,这意味着它们不会停止对 html 的解析,这意味着 onload-event 可以在脚本执行之前发生。我不想那样。但我确实想使用 type="module" 使我未导出的 JavaScript 函数和变量对同一页面上的其他脚本不可见。
我尝试了不同的选项,但由于上述答案,我了解到如果将 async -attribute 添加到模块类型的脚本中,这意味着脚本会异步加载,但一旦加载它就会立即执行。
但就我而言,这是嵌入在 HTML 页面中的脚本。因此,这意味着不需要“异步”加载。它已经与页面一起加载,因为它已嵌入其中。因此,这种变化确实立即执行了——这就是我想要的。
所以我认为有必要指出这种特殊情况,因为它有点违反直觉:要立即执行嵌入式脚本,您必须将 ASYNC 属性添加到其标记中。
通常人们可能会认为“异步”意味着某件事是异步发生的,以不确定的顺序发生,而不是立即发生。但是要意识到“异步”会导致异步加载,但加载完成后会立即执行。嵌入脚本后,无需加载,因此您可以立即执行。
摘要:使用
<script type="module" async> ... </script>
获取嵌入到 HTML 页面的模块脚本以立即执行。
完美匹配您的查询!
如果没有一个解决方案适合您,请参考我从我身边开发的以下解决方案。
我也在寻找解决方案,但经过大量搜索后,我将我的代码总结如下,这对我来说非常有效!!
当您想要功能时,这很有用,例如在前一个脚本完全加载之后然后只加载下一个脚本!
只需创建一个名为 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()
!
如果这对您有用,请为解决方案投票!
谢谢!
defer
使解析器有机会更快地开始下载,同时仍推迟其执行。请注意,如果您有很多来自同一主机的脚本,那么更早开始下载实际上可能会减慢您的页面正在等待的同一主机(因为它们竞争带宽)的其他脚本的下载(不是 {1 }) 所以这可能是一把双刃剑。