如果一个脚本既有 async 又有 defer 属性,会发生什么情况?

15 阅读3分钟

如果一个脚本既有 async 又有 defer 属性,会发生什么情况?

核心答案

async 优先级更高,defer 会被忽略。 当一个 <script> 标签同时具有 asyncdefer 属性时,浏览器会按照 async 的行为执行——脚本并行下载,下载完成后立即执行,不保证执行顺序。

这是 HTML 规范明确定义的行为,defer 在这种情况下作为降级回退存在,用于兼容不支持 async 的老旧浏览器。

深入解析

HTML 规范中的优先级

根据 HTML Living Standard,浏览器处理 <script> 标签的逻辑如下:

if (脚本有 src 属性) {
    if (async 属性存在) {
         使用 async 模式
    } else if (defer 属性存在) {
         使用 defer 模式
    } else {
         使用传统阻塞模式
    }
}

关键点:async 的判断在 defer 之前,所以 async 优先。

为什么要这样设计?

这是一个优雅降级的设计:

浏览器支持情况行为
支持 async使用 async(忽略 defer)
不支持 async,支持 defer使用 defer
都不支持传统阻塞加载

在 async 刚推出时(约 2010 年),老版本 IE(IE9 及以下)不支持 async 但支持 defer。同时写两个属性可以让:

  • 现代浏览器使用 async
  • 老浏览器回退到 defer

三种模式对比

                    下载        执行时机              顺序保证    阻塞解析
无属性              阻塞        下载完立即执行                  
async              并行        下载完立即执行                  
defer              并行        DOM 解析完成后                 
async + defer      并行        下载完立即执行                  

常见误区

误区 1: "两个属性会产生某种组合效果"

  • 错误。不存在 "async-defer" 混合模式,只会选择其中一个

误区 2: "defer 会覆盖 async"

  • 错误。恰恰相反,async 优先级更高

误区 3: "现代开发中同时写两个属性有意义"

  • 基本没有意义了。async 的浏览器支持率已经非常高(IE10+),不需要 defer 作为回退

内联脚本的特殊情况

<!-- async 和 defer 对内联脚本无效 -->
<script async defer>
  console.log('我是内联脚本,async 和 defer 都被忽略');
</script>

asyncdefer 只对外部脚本(有 src 属性)有效。

代码示例

<!-- 同时有 async 和 defer -->
<script async defer src="script.js"></script>

<!-- 等价于(在现代浏览器中) -->
<script async src="script.js"></script>

验证行为的测试代码

<!DOCTYPE html>
<html>
<head>
  <script async defer src="a.js"></script> <!-- 输出 A -->
  <script async defer src="b.js"></script> <!-- 输出 B -->
  <script async defer src="c.js"></script> <!-- 输出 C -->
</head>
<body>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      console.log('DOMContentLoaded');
    });
  </script>
</body>
</html>

<!--
可能的输出顺序(取决于下载速度):
B, A, C, DOMContentLoaded
或
A, C, B, DOMContentLoaded
或其他任意顺序

如果是纯 defer,输出一定是:
A, B, C, DOMContentLoaded
-->

实际应用场景

<!-- 2010-2015 年的兼容性写法 -->
<script async defer src="analytics.js"></script>

<!-- 现代写法:直接用 async 或 defer -->
<!-- 独立脚本(如统计、广告)用 async -->
<script async src="analytics.js"></script>

<!-- 有依赖关系的脚本用 defer -->
<script defer src="vendor.js"></script>
<script defer src="app.js"></script>

面试技巧

可能的追问方向

  1. "为什么 async 优先级更高?"

    • 这是 HTML 规范的设计,目的是让 defer 作为 async 的降级回退
    • 体现了渐进增强/优雅降级的设计思想
  2. "现在还需要同时写两个属性吗?"

    • 基本不需要。async 支持率已经很高
    • 如果要兼容 IE9,应该用其他方案(如 polyfill 或条件注释)
  3. "module 类型的脚本呢?"

    • <script type="module"> 默认就是 defer 行为
    • 可以加 async 变成 async 行为
    • 不需要显式写 defer
  4. "动态创建的脚本呢?"

    • 动态创建的 <script> 默认是 async 行为
    • 可以设置 script.async = false 来改变

展示深度的回答方式

"当 async 和 defer 同时存在时,async 优先,defer 被忽略。这是 HTML 规范明确定义的行为,设计初衷是让 defer 作为 async 的降级回退——在 async 刚推出时,老版本 IE 不支持 async 但支持 defer,同时写两个属性可以实现优雅降级。不过在现代开发中,这种写法已经没有必要了。"

一句话总结

async + defer = async;defer 只是 async 的降级回退,现代开发中无需同时使用。