异步脚本何时触发onload事件?

3
以下代码片段来自Google API文档中的示例。这个代码片段有趣的部分是,在<head>中的两个<script async>onload事件处理程序是在后面的<body>中定义的。异步脚本中的onload事件只有在解析完<body>后才会触发吗?是否有任何规范提供了这样的保证?或者这段代码仅在暗示这两个特定的<head>脚本需要很长时间来获取和执行的前提下才正确?
<!DOCTYPE html>
<html>
<head>
  <script async defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
  <script async defer src="https://accounts.google.com/gsi/client" onload="gisInit()"></script>
</head>
<body>
  <script>

    function gapiLoad() {
       // do something 
    }

    function gisInit() {
       // do something 
    }

    // there are other stuffs...

  </script>
</body>
</html>

1
请返回以下两个网页的翻译文本:https://dev59.com/6VkS5IYBdhLWcg3wN0K_ 以及 https://javascript.info/script-async-defer - nullqube
2个回答

3

async并不保证文档解析完成后再执行脚本。它会并行加载脚本,并在尽可能快的时间内执行它,因此如果脚本的获取在解析器到达定义回调函数的<script>之前就已经完成,那将会是一个问题:

<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
<script async defer src="https://accounts.google.com/gsi/client" onload="gisInit()"></script>
<!-- This one is blocking for at least 3s -->
<script src="https://deelay.me/3000/https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function gapiLoad() {
   console.log("loaded");
}
function gisInit() {
   console.log("init");
}
</script>

defer 延迟执行脚本,但如果同时设置了deferasyncasync的行为会覆盖defer,因此我们必须确保只设置了 defer

<script defer src="https://apis.google.com/js/api.js" onload="gapiLoad()"></script>
<script defer src="https://accounts.google.com/gsi/client" onload="gisInit()"></script>
<!-- this one is blocking for at least 3s -->
<script src="https://deelay.me/3000/https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
function gapiLoad() {
   console.log("loaded");
}
function gisInit() {
   console.log("init");
}
</script>


@Kalido 你认为将onload事件附加到body标签上怎么样? - Maik Lowrey
他们可能不需要等待所有图像资源加载完成。 - Kaiido
@kaiido 当你说“他们不冒险面对这个问题”时,你是指onload处理程序只在body解析后才被调用吗?此外,根据这个SO ,当同时使用asyncdefer时,在现代浏览器中async优先,因此defer在这里没有效果。 - sgu
@sgu 哎呀!你说得完全正确,defer 标记被简单地忽略了。我总是搞错这个,因为我不明白他们为什么要这样做。这将在某一天咬我一口...至于属性,我指的是 HTML 解析器会在设置 src(或者紧接着之后)之前就附加处理程序,所以并行获取始终会在处理程序附加之后结束。 - Kaiido
@Kaiido 但是“并行获取将始终在处理程序附加后结束”这一事实并不能保证正确性。关键问题是当调用<script>onload处理程序时,函数gapiLoad是否已定义。请注意,函数gapiLoad<body>中定义,而脚本src在<head>中获取。换句话说,浏览器是否可能在完成解析<body>之前触发异步脚本的onload事件并调用gapiLoad - sgu
@sgu 再次正确了...我不知道我对这个问题有什么想法;-) 因此,如果脚本在解析器到达回调定义之前执行,那么它将失败。 - Kaiido

1
在HTML标签中,Onload EventHandler支持以下标签:<body>, <frame> (已弃用), <iframe>, <img>, <input type="image">, <link>, <script>, <style>, <track>, <svg>, <use>, <foreignObject>, <image>, <object>, <embed。(感谢@Kaiido完成此列表。)
如果你想确保你的JS仅在DOM加载时运行,你可以从body标签加载一个处理程序函数。
注意:当使用async和onload时,需要知道异步加载的脚本会延迟onload事件。通常,异步加载的脚本会加载其他脚本。

function start() {
  gapiLoad();
  gisInit();
}

function gapiLoad() {
  console.log(1)
  // do something 
}

function gisInit() {
  console.log(2)
  // do something 
}
    
    // there are other stuffs...
<head>
<script async defer src="https://apis.google.com/js/api.js" ></script>
<script async defer src="https://accounts.google.com/gsi/client"></script>
</head>

<body onload="start()">


1
<frame>已被弃用,onload也会在<track><svg><use><foreignObject><image><object><embed>等元素上触发,可能还有其他我现在忘记的元素。但这不是问题的重点。 - Kaiido
@Kaiido 谢谢你的提示。我会在这一点上更新我的答案。 - Maik Lowrey

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