普通 script、async、defer 和 type=module 的加载执行顺序是什么?
先理解原理和示例,再练习完整口述与追问。
回答要点
经典脚本默认阻塞解析;async 下载后尽快执行且不保证顺序;defer 保序并在 DOM 解析后执行;模块默认延后且有依赖图。
原理拆解
由 HTML 解析器遇到的普通经典外链 script 通常会阻塞后续 HTML 解析,等待下载并执行完成。加 async 后,脚本并行下载,下载完成且能执行时尽快执行,可能打断解析;多个 async 脚本不保证按标签顺序执行,DOMContentLoaded 也不以等待它们为前提。
经典外链脚本加 defer 后并行下载,等待文档解析完成后按文档顺序执行,DOMContentLoaded 会等待这些延后脚本执行。defer 对普通内联经典脚本不产生相同效果。type=module 脚本默认有类似延后行为,浏览器会加载依赖图,模块有自己的作用域、使用严格模式,同一模块通常只执行一次,跨域加载还要满足 CORS。模块加 async 会改为依赖就绪后尽快运行,不能再依赖原来的顺序。
动态插入的脚本不应直接套用解析器插入脚本的规则。存在前后依赖时,应使用模块显式导入,或用明确的加载完成 Promise 串联。动态导入和模块的顶层 await 还会引入额外异步等待;不要只凭“DOM 已解析”推断所有业务初始化都完成,应显式等待应用自己的就绪信号。
参考:MDN script。
示例与推演
两个 defer 经典脚本 A、B,即使 B 先下载好,执行通常仍按文档 A→B,且在 DOM 解析后;换成 async 后,完成先后可影响执行顺序。若 B 依赖 A 创建的全局对象,async 就可能出错;模块应使用显式 import 表达依赖,而非依赖碰巧下载顺序。
易错点
脚本下载不阻塞解析,不代表执行阶段也完全不占主线程。动态插入、内联脚本与模块不能全部套同一套 defer 规则。
参考回答可直接练习口述
由 HTML 解析器遇到的普通经典外链 script 通常会阻塞后续 HTML 解析,等待下载并执行完成。加 async 后,脚本并行下载,下载完成且能执行时尽快执行,可能打断解析;多个 async 脚本不保证按标签顺序执行,DOMContentLoaded 也不以等待它们为前提。