Skip to content

异步与阻塞

元信息

  • 目标:从"三个外域脚本输出什么"切入,理解互联网不可靠决定了脚本加载的确定性无法预设;掌握同步/async/defer 三种加载方式在下载、执行时机、顺序上的差异,分清同步异步×阻塞非阻塞两组概念
  • 关键概念:同步加载、异步加载、defer、async、阻塞渲染、DOMContentLoaded、动态加载脚本、同步/异步、阻塞/非阻塞
  • 关联阶段:cp3
  • 常见误区:以为脚本按书写顺序输出是理所当然(网络不可靠,async 顺序不确定);有依赖的脚本用 async(应 defer 保序);把同步/异步与阻塞/非阻塞混为一谈(一组说任务间关系,一组说是否卡住渲染)

问题

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

假设每个脚本都包含一个console.log('...'),输出的是自己的文件名,比如a.js输出的是a

请问:上述代码最终输出的是什么?

答案

  1. abc
  2. ab
  3. a
  4. 没有任何输出

因为互联网是不可靠的,可能因各种原因无法访问(网络故障,主机宕机),所以是否加载成功是难以预测或确定的

同步加载

当浏览器解析HTML文档时,默认遇到<script>标签会立即停止HTML解析,开始下载并执行JavaScript代码,执行完毕后继续解析HTML。这种行为称为"同步加载"和"阻塞渲染"。

JavaScript可能会操作DOM(文档对象模型),比如修改页面元素、添加事件监听器等。如果脚本执行时DOM还未构建完成,可能会导致错误。因此,浏览器默认采用同步方式确保脚本执行时DOM已经准备就绪。

异步加载

2.1 defer属性:延迟执行

defer属性告诉浏览器异步下载脚本,但延迟到HTML解析完毕后,DOMContentLoaded事件触发前执行。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

特点:

  • 脚本异步下载,不阻塞HTML解析
  • 延迟到HTML解析完毕后执行
  • 多个带defer属性的脚本按在HTML中的顺序执行
  • 适用于依赖DOM的脚本

执行时序图:

2.2 async属性:异步加载

async属性告诉浏览器异步下载脚本,不阻塞HTML解析。脚本下载完成后会立即执行,执行时会暂停HTML解析。

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

特点:

  • 脚本异步下载,不阻塞HTML解析
  • 下载完成后立即执行,执行时暂停HTML解析
  • 多个带async属性的脚本执行顺序不确定,取决于下载完成的速度
  • 适用于独立脚本,如统计分析代码

执行时序图:

3. 三种加载方式对比

理解的核心是浏览器对网页的三个关键步骤

  • 下载:连接服务器,读取网络流到本地
  • 解析:对html代码进行解析,如定位、布局、渲染等
  • 执行:运行js代码
属性下载方式执行时机执行顺序适用场景
无属性同步立即执行按HTML顺序依赖DOM且需要立即执行的脚本
async异步下载完成后立即执行不确定独立脚本,如统计分析
defer异步HTML解析完成后执行按HTML顺序依赖DOM但不需要立即执行的脚本

4. 推荐的脚本加载策略

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

4.2 动态加载脚本

除了使用HTML属性,我们还可以通过JavaScript动态加载脚本:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

5. 性能影响与最佳实践

5.1 页面加载性能

脚本加载方式直接影响页面加载性能:

  1. 同步脚本:增加页面白屏时间,用户体验差
  2. 异步脚本:减少页面加载时间,但可能执行时机不确定
  3. 延迟脚本:平衡加载时间和执行时机

5.2 最佳实践建议

  1. 关键路径优化:将关键CSS和少量关键JavaScript放在<head>中同步加载
  2. 非关键资源延迟:使用defer加载依赖DOM的非关键脚本
  3. 独立脚本异步:使用async加载独立脚本,如统计、广告等
  4. 脚本合并:减少HTTP请求数量,合并小脚本
  5. 脚本压缩:使用工具压缩JavaScript文件,减少下载时间

6. 常见问题与解决方案

6.1 脚本依赖问题

问题:多个脚本有依赖关系,如何确保执行顺序?

解决方案:使用defer属性,确保按HTML顺序执行:

<!DOCTYPE html>
<html>
<head>
  <title>Parcel Sandbox</title>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <h1>Hello world</h1>
</body>
</html>

6.2 DOM Ready

问题:脚本执行时DOM尚未准备好

解决方案:使用DOMContentLoaded事件或defer属性:

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

7. 总结

理解JavaScript脚本的加载方式对前端开发至关重要:

  • 同步加载(默认):阻塞渲染,确保脚本执行时DOM已准备好
  • 异步加载async):不阻塞下载,执行时机不确定,适用于独立脚本
  • 延迟加载defer):不阻塞下载,确保执行顺序,适用于依赖DOM的脚本

合理使用这些属性可以显著提升网页性能,改善用户体验。在实际开发中,应根据脚本特性和需求选择最合适的加载方式。

7.1 同步(Synchronous)与异步(Asynchronous)

同步是指任务按顺序执行,一个任务必须等待前一个任务完成后才能开始。就像排队买票,只有前面的人买完,后面的人才能开始购买。

异步是指任务可以并行执行,不需要等待其他任务完成。就像在餐厅点餐,多人可以同时点餐,不需要等待前面的人吃完。

import "./styles.css";

document.getElementById("app").innerHTML = `
<h1>Hello world</h1>
`;

7.2 阻塞(Blocking)与非阻塞(Non-blocking)

阻塞是指一个任务执行时会阻止其他任务的进行。在浏览器中,阻塞通常指脚本执行会暂停HTML解析和页面渲染。

非阻塞是指一个任务执行时不会阻止其他任务的进行。在浏览器中,非阻塞通常指脚本下载或执行不会影响HTML解析和页面渲染。

7.3 同步/异步与阻塞/非阻塞的组合

在JavaScript脚本加载中,这四个概念可以组合成不同的情况:

组合描述示例
同步阻塞脚本按顺序加载,执行时暂停HTML解析默认的<script>标签
异步阻塞脚本异步下载,但执行时暂停HTML解析带有async属性的脚本
异步非阻塞脚本异步下载,执行时不暂停HTML解析带有defer属性的脚本

9. 参考

思考

为什么网络软件总是采用异步调用?