解析流程

1. 解析 DOM(初始宏任务)

  • 这是页面加载的第一个宏任务

2. 遇到同步脚本

1
2
3
4
5
6
7
解析DOM → 遇到同步<script> → 暂停解析 → 执行同步代码

同步代码产生微任务/宏任务 → 加入相应队列

同步代码执行完成 → 立即清空微任务队列

恢复DOM解析

3. 遇到 defer/module 脚本

1
2
3
4
5
6
7
8
9
解析DOM → 遇到defer/module<script> → 继续解析,异步下载

DOM解析完成 → 等待所有defer/module下载完成

按文档顺序执行defer/module脚本

每个脚本执行后清空微任务队列

所有脚本执行完成 → 触发DOMContentLoaded

4. 遇到 async 脚本

1
2
3
4
5
6
7
解析DOM → 遇到async<script> → 继续解析,异步下载

脚本下载完成(无论DOM是否解析完)

立即作为新宏任务执行

执行后清空微任务队列

5. DOM 解析完成后

1
2
3
4
5
6
7
8
9
10
11
12
13
DOM解析完成(当前宏任务结束)

清空微任务队列(确保)

判断是否需要渲染
├─ 需要 → 执行渲染流程
│ ├─ requestAnimationFrame回调
│ ├─ 样式计算 → 布局 → 绘制 → 合成
│ └─ 渲染完成

不需要渲染或渲染完成

从宏任务队列取下一个宏任务执行

🎯 注意时机

1. 微任务执行时机

  • 每个同步脚本执行后立即清空微任务队列
  • DOM 解析完成后再次清空微任务队列
  • 这是理解事件循环的关键

2. 异步脚本区别

  • defer/module:DOM 解析后按顺序执行
  • async:下载完立即执行,顺序不确定
  • 这是选择脚本加载策略的基础

3. 渲染时机

  • DOM 解析完成后判断是否渲染
  • 如果需要才进入渲染流程
  • 不需要就直接取下一个宏任务

📝 补充几个微妙细节:

1. 渲染流程内部的微任务

1
2
3
4
5
6
7
8
9
10
11
12
// 如果在requestAnimationFrame中产生微任务
requestAnimationFrame(() => {
console.log("rAF回调");
Promise.resolve().then(() => {
console.log("rAF中的微任务");
});
});

// 执行顺序:
// 1. 执行rAF回调 → "rAF回调"
// 2. 立即清空微任务 → "rAF中的微任务"
// 3. 然后才执行渲染管道

2. DOMContentLoaded 时机

1
2
3
DOM解析完成 → 执行所有defer/module脚本 → 触发DOMContentLoaded

async脚本不影响DOMContentLoaded触发时间

3. 多个 async 脚本竞争

1
2
3
4
5
<script async src="fast.js"></script>
<!-- 下载快 -->
<script async src="slow.js"></script>
<!-- 下载慢 -->
<!-- 可能fast先执行,也可能slow先执行 -->

🔍 验证代码:

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html>
<body>
<script>
console.log("1. 开始解析DOM");

setTimeout(() => {
console.log("7. setTimeout宏任务");
}, 0);

Promise.resolve().then(() => {
console.log("3. 初始微任务");
});
</script>

<script defer>
console.log("4. defer脚本");
Promise.resolve().then(() => {
console.log("5. defer脚本的微任务");
});
</script>

<div>DOM内容</div>

<script async>
// 假设很快下载完
console.log("2. async脚本(下载快)");
</script>

<script>
console.log("6. 同步脚本");
</script>
</body>
</html>

预测输出:

1
2
3
4
5
6
7
1. 开始解析DOM
3. 初始微任务 ← 第一个<script>执行后清空微任务
2. async脚本(下载快) ← async下载完立即执行
4. defer脚本 ← DOM解析完成后执行
5. defer脚本的微任务 ← defer执行后清空微任务
6. 同步脚本 ← 继续解析遇到的同步脚本
7. setTimeout宏任务 ← 从宏队列取出执行

🏁 最终确认:

  1. 事件循环机制:宏任务 → 微任务 → 可能渲染 → 下一宏任务
  2. 脚本执行区别:同步、async、defer/module 的不同行为
  3. 微任务时机:JS 执行后立即执行,宏任务结束后再次执行
  4. 渲染时机:微任务后判断,需要则渲染
  5. 异步协调:下载与执行的协调机制

💡 记住这个核心模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
页面加载 → 解析DOM宏任务

遇到脚本:
├─ 同步:暂停解析,执行,清微任务
├─ async:下载完立即执行,清微任务
├─ defer/module:DOM解析后执行,清微任务

DOM解析完成

清空微任务队列

判断渲染 → 需要则渲染

取下一个宏任务 → 循环继续

补充说明:async 这个每个浏览器实现机制不同