第15章:浏览器开发 —— DOM、JSX 与资源管理
约 3 分钟 · 更新于 2026-09-01
第15章:浏览器开发 —— DOM、JSX 与资源管理
本章补齐官方 Handbook 的浏览器侧主题:DOM 类型层次、元素查询与收窄、JSX 类型检查,以及 using / await using 所表达的确定性资源释放。
一、DOM 类型来自 lib.dom.d.ts
浏览器 API 的类型通常由 TypeScript 标准库声明提供:
json
{
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
Node-only 项目不应无意加入 DOM 类型,否则可能让不存在的浏览器全局变量通过检查。
二、DOM 类型层次
常见关系:
text
EventTarget
└── Node
└── Element
└── HTMLElement
├── HTMLDivElement
├── HTMLInputElement
└── HTMLButtonElement
越具体的类型拥有越具体的属性。例如 value 属于输入元素,不属于所有 HTMLElement。
三、查询元素并处理不存在
ts
const element = document.getElementById("search");
// HTMLElement | null
if (!(element instanceof HTMLInputElement)) {
throw new Error("#search 必须是 input");
}
console.log(element.value);
这同时验证存在性和真实元素类型,比直接断言更可靠。
四、querySelector 泛型
ts
const form = document.querySelector<HTMLFormElement>("#search-form");
泛型帮助指定期望类型,但仍可能返回 null,且不会在运行时验证选择器匹配的是表单。对不受自己控制的 DOM 仍应使用 instanceof。
五、children 与 childNodes
- children:只包含元素节点。
- childNodes:还可能包含文本、注释等 Node。
ts
for (const child of element.children) {
// child: Element
}
选择 API 时应符合真实节点需求。
六、事件类型
ts
const input = document.querySelector("#search");
if (input instanceof HTMLInputElement) {
input.addEventListener("input", event => {
const target = event.currentTarget;
if (target instanceof HTMLInputElement) {
console.log(target.value);
}
});
}
不要无条件断言 event.target。事件可能冒泡,target 与注册监听器的元素不是同一概念。
七、JSX 编译配置
JSX 的解析和输出由 jsx 选项控制,具体值取决于框架和构建工具。.tsx 文件允许 JSX;尖括号类型断言在 TSX 中不可用,应使用 as。
tsx
const value = input as HTMLInputElement;
八、JSX 类型检查模型
JSX 需要确定:
- 内置元素的属性类型。
- 函数组件或类组件是否可调用/构造。
- Props 是否匹配。
- children 是否允许。
- JSX 表达式结果类型。
这些规则通常由框架提供的 JSX 命名空间和类型声明定义,因此 React、Preact、Vue JSX 等可能不同。
九、内置元素与组件
tsx
<button disabled={true}>保存</button>
button 属于 intrinsic element,由框架声明属性。
tsx
interface ButtonProps {
label: string;
onClick(): void;
}
function Button(props: ButtonProps) {
return <button onClick={props.onClick}>{props.label}</button>;
}
大写名称按值解析为组件,Props 根据函数参数或类实例推导。
十、children 的契约
不要默认所有组件都接受任意 children:
tsx
interface PanelProps {
title: string;
children: React.ReactNode;
}
如果组件不应该有 children,就不要为了方便添加宽泛字段。
十一、Symbol 与迭代协议
Symbol.iterator、Symbol.asyncIterator 等 well-known symbols 连接对象与语言协议。实现自定义迭代器时,需要正确返回迭代结果和结束状态;一般业务优先使用数组、Map、Set 和生成器。
unique symbol 可作为真正唯一的类型级键:
ts
const token: unique symbol = Symbol("token");
十二、using 与 await using
支持显式资源管理的对象可实现:
ts
interface Disposable {
[Symbol.dispose](): void;
}
ts
using resource = openResource();
// 离开作用域时调用 Symbol.dispose
异步资源:
ts
await using connection = await openConnection();
这与普通 await 不同:await using 会在离开作用域时等待异步清理。
运行环境支持
语法能否编译、输出如何降级、运行时是否具备相关 Symbol 与辅助代码,取决于 TypeScript 配置和目标环境。必须验证真实产物。
十三、资源生命周期
浏览器资源不只包括文件:
- 事件监听器。
- 定时器。
- WebSocket。
- Observer。
- AbortController。
- MediaStream。
框架组件卸载或作用域结束时必须清理。类型能帮助定义清理函数:
ts
function subscribe(listener: () => void): () => void {
// 注册
return () => {
// 取消
};
}
十四、常见误区
- DOM 查询后直接非空断言。
- 用泛型 querySelector 代替运行时验证。
- 混淆 target 与 currentTarget。
- Node 项目意外依赖 DOM 全局类型。
- 认为所有 JSX 框架共享 React 的类型规则。
- 所有组件都宽泛接受 children。
- 添加监听器、定时器或连接后不清理。
- 使用 using 语法但不验证目标运行环境。
十五、练习
- 安全查询输入框并读取值,不使用 !。
- 比较 children 和 childNodes。
- 创建一个带严格 Props 的 TSX 组件。
- 实现返回取消函数的事件订阅 API。
- 编写实现 Symbol.dispose 的测试资源并观察清理顺序。
十六、总结
- DOM 类型层次对应浏览器对象继承关系。
- 查询 API 通常返回可空和较宽类型,需要运行时收窄。
- JSX 类型规则由具体框架声明提供。
- Symbol 连接语言协议,using 表达确定性资源释放。
- 监听器、定时器和连接都需要明确生命周期。
请继续阅读:第16章:构建与工具链。
原始资料引用
- 第14章:JavaScript 渐进类型化
- 第16章:构建与工具链