内联脚本执行时 dom 尚未构建完成,document.getelementbyid 返回 null,这是 html 解析器线性推进、遇 暂停 dom 构建的必然结果;fetch 等异步逻辑在解析阶段不预加载、不发起请求,仅注册回调,真正执行依赖事件循环。

内联脚本执行时 DOM 尚未构建完成,document.getElementById 返回 null
这不是异步的问题,而是 HTML 解析器线性推进、遇到 <script></script> 就暂停构建 DOM 的必然结果。比如在 里写 document.getElementById('app'),此时 <div id="app"> 根本还没被读到,DOM 树里压根没有这个节点。
<p>常见错误场景包括:</p>
<ul>
<li>把初始化逻辑直接写在 <code> 内联脚本中,且未加任何时机控制
setTimeout 或 Promise.resolve().then() 能“等 DOM”,其实它们只是注册回调,执行仍发生在 JS 执行完之后,而此时 DOM 可能依然不完整document.readyState === 'loading' 判断后就查元素,但该状态只表示解析中,不代表目标元素已出现可靠解法只有两个:
① 把内联脚本移到目标元素之后(如放在 前);
② 显式监听 DOMContentLoaded 事件,或用 document.addEventListener('readystatechange', () => { if (document.readyState === 'interactive') { ... } })。
内联脚本里的 fetch 不会提前发起请求
fetch 写在 <script></script> 标签里,和写在外部文件里一样,**HTML 解析阶段完全不预加载、不提前调度、不发起任何网络请求**。预扫描器(Preload Scanner)只识别静态 HTML 中的 <link rel="preload">、<img src>、<script src></script>,根本不会去解析 JS 字符串内容。
实际行为是:
- 解析器遇到
<script>fetch('/api/user')</script>,立刻暂停 DOM 构建,同步执行 JS -
fetch此时只是注册一个 Promise 回调,不发请求 - 请求真正发出的时间点 = JS 执行完毕 + 控制权交还事件循环之后
- 若脚本在
,请求可能比开始解析还早;若在中段,则必须等前面所有 HTML 解析完才执行
想让请求更早发出?别靠内联 fetch,改用:<link rel="preload" href="/api/user" as="fetch" crossorigin>——浏览器在解析 HTML 字节流时就发起,早于任何 JS 执行。
内联脚本出错会中断 DOM 解析
现代浏览器中,普通 JS 运行时错误(如 ReferenceError)默认不中断 DOM 解析,但有三个例外:一是 document.write() 调用,二是 defer/async 脚本加载阶段的语法错误,三是旧版 IE 的特定行为。其中 document.write() 是唯一真正能“重置解析器、丢弃已解析节点”的常见操作。
典型破坏路径:
-
<script>document.getElementById('x').addEventListener('click', fn)</script>放在→ 报Cannot read property 'addEventListener' of null→ 解析中断 → 后续所有标签不进 DOM - 内联脚本含
while(true){}或超大数组排序 → 主线程冻结 → 解析器无法继续推进 - 第三方 SDK 初始化失败并
throw错误,且没包try/catch→ 错误冒泡到全局,阻断后续解析
修复关键不是事后捕获,而是从源头隔离风险:
① 避免在 写任何依赖 DOM 的内联 JS;
② 必须写时,统一包裹在 document.addEventListener('DOMContentLoaded', () => { ... });
③ 彻底移除 document.write(),它在现代项目中已无合理使用场景。
defer 对内联脚本无效,混用 async 会静默丢弃 defer
defer 属性只对外部脚本有效,前提是同时满足:有 src 属性 + defer 布尔属性。写成 <script defer>console.log(1)</script>,浏览器直接忽略 defer,按同步脚本处理——依然阻塞解析。
容易踩的坑:
-
<script src="a.js" defer></script>:赋值写法无效,defer是布尔属性,不能带值 -
<script src="b.js" async defer></script>:async和defer混用时,defer被静默丢弃,按async规则执行 - 构建工具(如 Vite)可能把
defer脚本转为内联,导致defer失效且无提示
真正起作用的写法只有一种:<script src="utils.js" defer></script>。它保证异步下载、DOM 解析完成后按序执行,可安全访问完整 DOM,但不等图片、样式表或 iframe 加载完成。











