HTML DOM Script defer 属性(手把手讲解)
💡一则或许对你有用的小广告
欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...
,点击查看项目介绍 ;演示链接: http://116.62.199.48:7070 ;- 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;
截止目前, 星球 内专栏累计输出 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 引入了 async
和 defer
属性。两者的共同目标是让脚本异步加载,但执行时机不同:
| 属性 | 加载行为 | 执行时机 | 执行顺序保证 |
|---------|-------------------------|-----------------------------------|--------------|
| async | 并行下载,独立于 DOM 解析 | DOM 解析完成后立即执行(可能中途插入) | 无 |
| defer | 并行下载,独立于 DOM 解析 | DOM 解析完成后按加载顺序执行 | 有 |
defer 属性的核心机制与执行流程
1. defer 的执行阶段解析
defer
属性的核心逻辑可分解为三个阶段:
- 下载阶段:浏览器在解析 HTML 时,发现带有
defer
的<script>
标签,会立即开始下载脚本,但不会阻塞 DOM 解析。 - 排队阶段:下载完成后,脚本会被放入一个“延迟队列”。此时 DOM 继续构建,直到页面解析完成(触发
DOMContentLoaded
事件)。 - 执行阶段:所有
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.js
和b.js
可能以任意顺序执行,甚至在 DOM 完全加载前执行。 -
defer:严格等待 DOM 解析完成,且按加载顺序执行。
2. 适用场景的决策树
场景描述 | 推荐属性 | 原因 |
---|---|---|
脚本需立即执行且不依赖 DOM | async | 减少阻塞,但需处理 DOM 未就绪 |
脚本需操作 DOM 且存在顺序依赖 | defer | 保证 DOM 完整和顺序可控 |
脚本间无依赖且无需 DOM 操作 | async 或 defer | 选择更轻量的 async |
3. 混合使用时的注意事项
避免同一页面同时使用 async
和 defer
的脚本,因为它们的执行顺序无法预测。例如:
<script async src="a.js"></script>
<script defer src="b.js"></script>
a.js
可能早于、晚于或与 b.js
交错执行。
深入理解:defer 的底层实现原理
1. 浏览器事件循环与脚本执行
当浏览器遇到 defer
脚本时,会通过以下步骤处理:
- 下载:在解析 HTML 时启动下载,但不阻塞主线程。
- 排队:下载完成后,将脚本存入
defer
队列。 - 触发执行:在 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
的不确定性。开发者应根据脚本的依赖关系、执行时机需求,结合 defer
与 async
,设计出性能高效且逻辑可靠的页面结构。
在现代前端工程化中,defer
仍是优化加载策略的基础工具之一。理解其底层机制与使用场景,能帮助开发者避免常见错误,并为更复杂的异步加载策略(如 Webpack 的代码分割)奠定基础。