HTML DOM Script defer 属性(手把手讲解)

更新时间:

💡一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

截止目前, 星球 内专栏累计输出 90w+ 字,讲解图 3441+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 3100+ 小伙伴加入学习 ,欢迎点击围观

前言:脚本加载与DOM构建的平衡艺术

在网页开发中,JavaScript 与 HTML DOM 的交互是核心之一。但脚本的加载方式直接影响页面性能与代码逻辑的执行顺序。例如,若脚本在 DOM 完全加载前执行,可能会因找不到元素而报错。此时,defer 属性便成为解决这一问题的关键工具。本文将从基础概念、执行机制、与 DOM 的交互关系,以及实际案例入手,深入解析这一属性的使用场景与技术细节。


HTML Script 标签与脚本执行的默认行为

1. 脚本加载对页面渲染的影响

当浏览器解析到 <script> 标签时,默认会暂停 DOM 的构建,立即下载并执行脚本。这种同步阻塞行为可能导致页面渲染延迟,用户体验下降。例如:

<body>  
  <script>  
    // 假设此处有耗时操作  
    document.write('Hello');  
  </script>  
  <div>页面内容</div>  
</body>  

此时,<div> 元素的渲染会被延迟,直到脚本执行完毕。

2. 非阻塞加载的两种方式:async 与 defer

为缓解阻塞问题,HTML5 引入了 asyncdefer 属性。两者的共同目标是让脚本异步加载,但执行时机不同:
| 属性 | 加载行为 | 执行时机 | 执行顺序保证 |
|---------|-------------------------|-----------------------------------|--------------|
| async | 并行下载,独立于 DOM 解析 | DOM 解析完成后立即执行(可能中途插入) | 无 |
| defer | 并行下载,独立于 DOM 解析 | DOM 解析完成后按加载顺序执行 | 有 |


defer 属性的核心机制与执行流程

1. defer 的执行阶段解析

defer 属性的核心逻辑可分解为三个阶段:

  1. 下载阶段:浏览器在解析 HTML 时,发现带有 defer<script> 标签,会立即开始下载脚本,但不会阻塞 DOM 解析。
  2. 排队阶段:下载完成后,脚本会被放入一个“延迟队列”。此时 DOM 继续构建,直到页面解析完成(触发 DOMContentLoaded 事件)。
  3. 执行阶段:所有 defer 脚本按加载顺序依次执行,且在 DOMContentLoaded 事件触发前完成。

2. 与 DOM 的交互:脚本执行时 DOM 已就绪

由于 defer 脚本在 DOM 完全解析后执行,此时页面元素已存在。例如:

<script defer src="script1.js"></script>  
<script defer src="script2.js"></script>  

script1.js 中可以直接操作页面中的元素,无需担心“元素未定义”的错误。这与直接在 <body> 中插入 <script> 标签形成鲜明对比。


defer 在实际开发中的典型应用场景

1. 第三方脚本的非阻塞加载

对于无法修改的第三方脚本(如统计代码或广告代码),添加 defer 可避免其阻塞 DOM 构建:

<!-- 传统写法(阻塞 DOM) -->  
<script src="third-party.js"></script>  

<!-- defer 改进后 -->  
<script defer src="third-party.js"></script>  

此案例中,脚本仍会执行,但页面渲染不再被延迟。

2. 模块化脚本的顺序依赖

若多个脚本存在依赖关系(如 A.js 需要 B.js 定义的函数),可通过 defer 保证执行顺序:

<script defer src="lib.js"></script>  
<script defer src="main.js"></script>  

由于 defer 脚本按加载顺序执行,main.js 可安全调用 lib.js 中的函数。

3. 动态 DOM 操作的示例

假设需在页面加载后动态添加元素,使用 defer 可确保 DOM 已就绪:

<div id="target"></div>  
<script defer>  
  document.getElementById('target').innerHTML = '<p>内容已加载</p>';  
</script>  

若移除 defer,脚本可能因 #target 未解析而报错。


defer 与 async 的对比:选择的黄金法则

1. 执行时机的差异

  • async:脚本下载完成后立即执行,可能在 DOM 解析中途插入。例如:

    <script async src="a.js"></script>  
    <script async src="b.js"></script>  
    

    a.jsb.js 可能以任意顺序执行,甚至在 DOM 完全加载前执行。

  • defer:严格等待 DOM 解析完成,且按加载顺序执行。

2. 适用场景的决策树

场景描述推荐属性原因
脚本需立即执行且不依赖 DOMasync减少阻塞,但需处理 DOM 未就绪
脚本需操作 DOM 且存在顺序依赖defer保证 DOM 完整和顺序可控
脚本间无依赖且无需 DOM 操作async 或 defer选择更轻量的 async

3. 混合使用时的注意事项

避免同一页面同时使用 asyncdefer 的脚本,因为它们的执行顺序无法预测。例如:

<script async src="a.js"></script>  
<script defer src="b.js"></script>  

a.js 可能早于、晚于或与 b.js 交错执行。


深入理解:defer 的底层实现原理

1. 浏览器事件循环与脚本执行

当浏览器遇到 defer 脚本时,会通过以下步骤处理:

  1. 下载:在解析 HTML 时启动下载,但不阻塞主线程。
  2. 排队:下载完成后,将脚本存入 defer 队列。
  3. 触发执行:在 HTML 解析完成(即 document.readyState 变为 interactive)时,遍历队列并按顺序执行脚本。

2. 与 DOMContentLoaded 事件的关系

所有 defer 脚本的执行DOMContentLoaded 触发前完成。因此,若在事件监听器中操作 DOM,可能覆盖 defer 脚本的结果:

document.addEventListener('DOMContentLoaded', () => {  
  // 此处代码可能在 defer 脚本之后执行  
});  

需注意执行顺序的优先级。


高级技巧:结合动态脚本与 defer 的进阶用法

1. 动态创建脚本元素时模拟 defer

若需通过 JavaScript 动态加载脚本,可通过设置 defer 属性实现类似行为:

const script = document.createElement('script');  
script.src = 'dynamic.js';  
script.defer = true;  // 显式设置 defer  
document.body.appendChild(script);  

2. 处理 defer 脚本的依赖链

当多个 defer 脚本形成依赖时,确保 HTML 中的加载顺序与依赖关系一致:

<!-- lib.js 定义工具函数 -->  
<script defer src="lib.js"></script>  
<!-- main.js 调用 lib.js 的函数 -->  
<script defer src="main.js"></script>  

结论:合理使用 defer 提升性能与代码健壮性

defer 属性通过异步加载与有序执行,解决了传统脚本阻塞 DOM 构建的问题。对于需要操作 DOM 的脚本,它提供了“安全执行环境”的保障,同时避免了 async 的不确定性。开发者应根据脚本的依赖关系、执行时机需求,结合 deferasync,设计出性能高效且逻辑可靠的页面结构。

在现代前端工程化中,defer 仍是优化加载策略的基础工具之一。理解其底层机制与使用场景,能帮助开发者避免常见错误,并为更复杂的异步加载策略(如 Webpack 的代码分割)奠定基础。

最新发布