HTML 与 DOM
HTML5 新增
语义化标签
HTML5 引入语义化标签来替代无意义的 <div>,提升可读性、SEO 和无障碍访问。
| 标签 | 含义 | 使用要点 |
|---|---|---|
<header> |
页眉/区块头部 | 可包含 logo、导航、搜索栏;一个页面可有多个 |
<nav> |
导航链接区域 | 用于主导航,不要给所有链接组都加 nav |
<main> |
页面主体内容 | 每个页面唯一,不应包含侧栏、导航等重复内容 |
<article> |
独立内容单元 | 论坛帖子、新闻文章、评论;可嵌套 section |
<section> |
主题分组 | 通常含标题,不要作为普通 div 容器使用 |
<aside> |
侧边内容 | 与主内容间接相关(侧栏、广告、相关链接) |
<footer> |
页脚/区块尾部 | 版权、作者信息、相关链接 |
SEO 与 A11y 收益:屏幕阅读器可直接跳转到 nav/main 等区域,搜索引擎根据语义标签提升内容权重。
表单增强
新 input 类型:
<!-- 浏览器会提供对应的键盘/校验/控件 -->
<input type="email"> <!-- 邮箱格式校验 + 移动端@键盘 -->
<input type="url"> <!-- URL 格式校验 -->
<input type="tel"> <!-- 移动端数字键盘 -->
<input type="number"> <!-- 数字步进控件 -->
<input type="range"> <!-- 滑块选择 -->
<input type="color"> <!-- 颜色选择器 -->
<input type="date"> <!-- 日期选择器 -->
<input type="datetime-local">
<input type="month">
<input type="week">
<input type="time">
<input type="search"> <!-- 搜索框(样式差异) -->
新增属性:
| 属性 | 作用 |
|---|---|
required |
非空校验,替代 JS 校验的基础 |
pattern="[A-Z]{3}" |
正则校验 |
autocomplete="off" |
控制自动填充行为 |
autofocus |
页面加载时自动聚焦 |
placeholder |
占位提示(注意不要代替 label) |
min/max/step |
数值/日期范围限制 |
multiple |
文件/邮箱多选 |
novalidate |
禁用表单校验(用于草稿保存) |
注意:浏览器校验样式不可控,通常使用 :invalid / :valid 伪类自定义样式,或用 novalidate 配合自定义校验。
多媒体标签
<audio controls>
<source src="music.mp3" type="audio/mpeg">
<source src="music.ogg" type="audio/ogg">
您的浏览器不支持 audio 标签
</audio>
<video controls width="720" poster="thumbnail.jpg">
<source src="movie.mp4" type="video/mp4">
<source src="movie.webm" type="video/webm">
<track src="subtitles.vtt" kind="subtitles" srclang="zh" label="中文">
</video>
关键属性:controls、autoplay(移动端受限)、loop、muted(自动播放必须静音)、preload="none|metadata|auto"。
Canvas vs SVG
| 维度 | Canvas | SVG |
|---|---|---|
| 本质 | 位图(像素操作) | 矢量图(DOM 节点) |
| 渲染方式 | 即时模式(逐帧绘制) | 保留模式(树形结构) |
| 事件处理 | 不支持 | 支持(每个元素是 DOM 节点) |
| 性能(大量元素) | 优 | 差 |
| 高清屏适配 | 需手动 devicePixelRatio | 自动适配 |
| 适用场景 | 游戏、图像处理、图表 | 图标、交互式图形、动画 |
Canvas 示例:
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// 必须手动处理高清屏
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);
Web Storage
// Cookie —— 每次请求自动携带,4KB 限制
document.cookie = "name=value; max-age=3600; path=/; SameSite=Lax";
// localStorage —— 持久存储(除非手动清除),5MB+,同源共享
localStorage.setItem('key', 'value');
localStorage.getItem('key');
localStorage.removeItem('key');
// sessionStorage —— 标签页级隔离,关闭标签页即清除
sessionStorage.setItem('key', 'value');
三者对比:
| 特性 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | ~4KB | ~5MB+ | ~5MB+ |
| 作用域 | 同源所有标签页 | 同源所有标签页 | 同源当前标签页(同源同标签页 iframe 共享) |
| 寿命 | 可设过期时间 | 持久 | 标签页关闭即清除 |
| HTTP 请求 | 自动携带 | 不携带 | 不携带 |
| 操作 API | 原始字符串 | key-value | key-value |
| SameSite 安全 | 支持 | — | — |
WebSocket
const ws = new WebSocket('wss://example.com/chat');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息:', data);
};
ws.onclose = (event) => {
if (event.code === 1006) {
// 异常断开,尝试重连
reconnect();
}
};
ws.onerror = (err) => {
console.error('WebSocket 错误:', err);
};
心跳机制:为防止网关/代理断开空闲连接,通常每 30-60s 发送 ping 帧。
对比 HTTP 轮询:建立后全双工通信,头开销只有几个字节 vs HTTP 几百字节。
Service Worker
生命周期:
注册 → 安装(install) → 激活(activate) → 运行(fetch/message)
↓ (失败则等待下次)
// 注册(必须在主线程)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW 注册成功', reg.scope))
.catch(err => console.error('SW 注册失败', err));
}
SW 内代码:
// sw.js
const CACHE_NAME = 'v1';
const URLS_TO_CACHE = ['/', '/styles.css', '/app.js'];
// 安装:预缓存资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(URLS_TO_CACHE))
);
// 强制等待中的 SW 激活
self.skipWaiting();
});
// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
// 立即接管所有页面
self.clients.claim();
});
// 请求拦截:四种缓存策略
self.addEventListener('fetch', (event) => {
event.respondWith(
// 策略 1:Cache First —— 静态资源
caches.match(event.request).then(resp => resp || fetch(event.request))
// 策略 2:Network First —— API 请求
// fetch(event.request).catch(() => caches.match(event.request))
// 策略 3:Stale While Revalidate —— 优先缓存 + 后台更新
// ...
// 策略 4:Network Only —— 需要实时数据
);
});
SW 限制:
- 必须 HTTPS(localhost 除外)
- 没有 DOM 访问权限
- 不能使用同步 API(如 XHR synchronous)
- 作用域:默认在
sw.js所在目录
History API
// 不刷新页面前进(SPA 路由核心)
history.pushState({ page: 1 }, '标题', '/page1');
history.replaceState({ page: 2 }, '标题', '/page2');
// 监听 popstate(仅后退/前进触发,pushState/replaceState 不触发)
window.addEventListener('popstate', (event) => {
console.log('状态变化:', event.state);
renderRoute(location.pathname);
});
// 与 hash 路由比较
// pushState URL 更干净,但服务端需要 fallback 到 index.html
// hash 路由兼容旧浏览器,服务端无需配置
DOM 操作
节点类型
document.nodeType; // 9 — Document
element.nodeType; // 1 — Element
attr.nodeType; // 2 — Attribute(已废弃)
text.nodeType; // 3 — Text
comment.nodeType; // 8 — Comment
documentFragment.nodeType; // 11 — DocumentFragment
DocumentFragment:批量操作避免回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
ul.appendChild(fragment); // 一次回流
增删改查 API
// 查询
document.getElementById('id');
document.querySelector('.class'); // 第一个匹配
document.querySelectorAll('.class'); // NodeList(静态快照)
document.elementFromPoint(x, y); // 点击测试
elem.closest('.parent'); // 向上查找
elem.matches('.active'); // 是否匹配
// 创建
document.createElement('div');
document.createTextNode('text');
document.createDocumentFragment();
elem.cloneNode(true); // 深拷贝(含子节点)
// 插入
parent.appendChild(child);
parent.insertBefore(newChild, refChild);
parent.prepend(child); // 开头插入
parent.append(child); // 末尾插入(可传多个)
child.after(newSibling); // 兄弟节点后插入
child.before(newSibling);
elem.insertAdjacentHTML('beforeend', '<span>HTML</span>');
// 可选:'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'
// 替换/删除
parent.replaceChild(newChild, oldChild);
elem.remove(); // 直接删除自身
parent.removeChild(child);
性能注意:
querySelectorAll返回静态快照,getElementsByTagName返回动态集合(性能更好但行为有陷阱)- 频繁 DOM 操作使用 DocumentFragment 或
innerHTML批量构建
事件流
捕获阶段 (Capture) → 目标阶段 (Target) → 冒泡阶段 (Bubble)
↓ ↓ ↓
window → document → parent → target → parent → document → window
addEventListener 第三个参数:
// capture: true —— 在捕获阶段触发
elem.addEventListener('click', handler, { capture: true });
// 等价于
elem.addEventListener('click', handler, true);
// once: true —— 自动移除(无需手动 removeEventListener)
elem.addEventListener('click', handler, { once: true });
// passive: true —— 声明不会调用 preventDefault
// 对 touchstart/touchmove 提升滚动性能(Chrome 66+ 默认 passive: true)
elem.addEventListener('touchstart', handler, { passive: true });
事件对象方法:
event.preventDefault(); // 阻止默认行为(如链接跳转、表单提交)
event.stopPropagation(); // 阻止事件传播(捕获+冒泡)
event.stopImmediatePropagation(); // 同上 + 阻止当前元素其余事件处理
// event.target vs event.currentTarget
// target: 触发事件的元素
// currentTarget: 绑定事件处理器的元素 (this)
事件委托(事件代理)
将事件监听器绑定到父元素,利用事件冒泡处理子元素事件。
// ❌ 低效:每个 li 绑定一个监听器
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', () => handleClick(li));
});
// ✅ 高效:一个监听器处理所有
document.querySelector('ul').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return; // 不是 li 点击,忽略
handleClick(li.dataset.id);
});
优势:减少内存占用、动态添加的元素自动生效。
限制:不适用于不冒泡的事件(focus、blur、scroll、mouseenter、mouseleave)。
替代方案:focus/blur 用 focusin/focusout(可冒泡)。
自定义事件
// 创建
const event = new CustomEvent('userLogin', {
detail: { userId: 123, name: 'Alice' },
bubbles: true,
cancelable: true,
});
// 派发
elem.dispatchEvent(event);
// 监听
elem.addEventListener('userLogin', (e) => {
console.log(e.detail); // { userId: 123, name: 'Alice' }
});
DOM 遍历
// NodeIterator —— 简单遍历
const iterator = document.createNodeIterator(
root,
NodeFilter.SHOW_ELEMENT, // 只遍历元素节点
{
acceptNode(node) {
return node.classList.contains('skip')
? NodeFilter.FILTER_REJECT // 跳过该节点及其子节点
: NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while ((node = iterator.nextNode())) {
console.log(node.tagName);
}
// TreeWalker —— 更灵活(支持 parent/sibling 导航)
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_TEXT,
null,
false // entityReferenceExpansion
);
while ((node = walker.nextNode())) {
console.log(node.textContent.trim());
}
事件模型
addEventListener 第三个参数详解
// 对象形式(推荐)
elem.addEventListener('click', handler, {
capture: false, // 捕获阶段触发
once: false, // 只执行一次后自动移除
passive: false, // 不调用 preventDefault(滚动性能优化)
signal: abortController.signal, // 与 AbortController 配合移除
});
// 使用 AbortController 批量移除
const controller = new AbortController();
elem.addEventListener('click', handler1, { signal: controller.signal });
elem.addEventListener('scroll', handler2, { signal: controller.signal });
controller.abort(); // 同时移除两个
preventDefault vs stopPropagation vs stopImmediatePropagation
| 方法 | 阻止默认行为 | 阻止传播 | 阻止同级其他监听 | 可取消判断 |
|---|---|---|---|---|
preventDefault() |
是 | 否 | 否 | event.cancelable |
stopPropagation() |
否 | 是 | 否 | — |
stopImmediatePropagation() |
否 | 是 | 是 | — |
return false(jQuery) |
是 | 是 | 否 | — |
最佳实践:不要滥用 stopPropagation,可能破坏页面级事件委托。如确实需要阻止,考虑判断 event.target 自行过滤。
事件代理的深入理解
// 一个真实的菜单事件代理
menu.addEventListener('click', (e) => {
const item = e.target.closest('[data-action]');
if (!item) return;
const action = item.dataset.action;
e.preventDefault(); // 阻止链接跳转
switch (action) {
case 'edit': openEditor(item.dataset.id); break;
case 'delete': confirmDelete(item.dataset.id); break;
case 'share': openShareDialog(item.dataset.id); break;
}
});
Web Components
Custom Elements
// 自定义元素(必须包含连字符)
class MyButton extends HTMLElement {
// 指定需要观察的属性变化
static get observedAttributes() {
return ['disabled', 'variant'];
}
constructor() {
super();
// 初始化状态,但不要访问属性或 DOM
this._variant = 'primary';
}
// 元素被插入 DOM
connectedCallback() {
this.render();
console.log('组件已挂载');
}
// 元素从 DOM 移除
disconnectedCallback() {
console.log('组件已卸载,清理定时器/事件');
}
// 属性变化时触发
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'variant') this._variant = newValue;
this.render();
}
render() {
this.innerHTML = `
<button class="btn btn-${this._variant}" ${this.getAttribute('disabled') !== null ? 'disabled' : ''}>
<slot></slot>
</button>
`;
}
}
customElements.define('my-button', MyButton);
生命周期:
constructor()—— 元素创建(不能访问 DOM)connectedCallback()—— 插入 DOMdisconnectedCallback()—— 从 DOM 移除attributeChangedCallback(name, old, new)——observedAttributes中属性变化adoptedCallback()—— 被移动至新文档
Shadow DOM
class MyCard extends HTMLElement {
constructor() {
super();
// 创建 Shadow DOM
this.attachShadow({ mode: 'open' }); // open: 外部可通过 element.shadowRoot 访问
// mode: 'closed' —— 外部无法访问 shadowRoot(但 inspect element 仍可查看)
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host { /* :host 指代宿主元素本身 */
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
}
:host([highlight]) { /* 宿主有 highlight 属性时 */
border-color: gold;
}
:host-context(.dark) { /* 宿主在 .dark 容器内 */
background: #333;
color: white;
}
::slotted(h2) { /* 插槽内容样式 */
margin-top: 0;
}
.card-content {
margin-top: 8px;
}
</style>
<slot name="title">默认标题</slot>
<div class="card-content">
<slot></slot> <!-- 默认插槽 -->
</div>
`;
// 样式隔离:外部样式不会穿透 shadow DOM
// 外部可使用 CSS 自定义属性进行主题定制
}
}
customElements.define('my-card', MyCard);
<my-card highlight>
<h2 slot="title">Card 标题</h2>
<p>这是卡片内容,外部样式不影响内部。</p>
</my-card>
HTML Templates
<template id="user-card-tpl">
<style>
.user-card { display: flex; align-items: center; gap: 12px; }
.avatar { width: 48px; height: 48px; border-radius: 50%; }
.name { font-weight: bold; }
</style>
<div class="user-card">
<img class="avatar" src="" alt="avatar">
<div class="name"></div>
</div>
</template>
class UserCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById('user-card-tpl');
// template.content 是 DocumentFragment
const clone = template.content.cloneNode(true); // 每次克隆新实例
clone.querySelector('.avatar').src = this.getAttribute('avatar');
clone.querySelector('.name').textContent = this.getAttribute('name');
this.attachShadow({ mode: 'open' }).appendChild(clone);
}
}
customElements.define('user-card', UserCard);
可访问性(A11y)
ARIA 属性
<!-- 角色(role) -->
<div role="button" tabindex="0" @click="handleClick" @keydown.enter="handleClick">
自定义按钮
</div>
<!-- 状态与属性 -->
<div role="alert" aria-live="assertive">操作成功</div>
<input aria-label="搜索关键词" aria-required="true" aria-invalid="false">
<div aria-expanded="false" aria-controls="menu-panel">菜单</div>
<div role="progressbar" aria-valuenow="30" aria-valuemin="0" aria-valuemax="100">
ARIA 使用原则(WAI-ARIA 第一原则):
- 不要使用 ARIA 如果可以用原生 HTML 语义元素
- 不要改变原生语义(如给
<h2>加role="button") - 所有交互式 ARIA 角色必须支持键盘操作
语义化 HTML 与屏幕阅读器
| 非语义方式 | 语义化方式 | 效果 |
|---|---|---|
<div class="nav"> |
<nav> |
自动识别导航区域 |
<div class="footer"> |
<footer> |
自动识别页脚 |
<div onclick="..."> |
<button> |
可聚焦、可键盘操作、有角色 |
<div class="heading"> 加 CSS |
<h1>~<h6> |
标题层级导航 |
<img alt=""> |
始终提供 alt | 图片描述 |
<span class="required">* |
<input required> + aria-required |
浏览器自动提示 |
Landmark 角色:banner、navigation、main、complementary、contentinfo。显式 role 在旧浏览器中仍有价值,新浏览器可直接使用语义标签。
键盘导航
<!-- tabindex 控制 -->
<button>可访问</button> <!-- 默认 0,自然顺序 -->
<div tabindex="0">编程焦点</div> <!-- 加入 Tab 键顺序 -->
<div tabindex="-1">可 JS 聚焦</div> <!-- Tab 键跳过,但可 focus() -->
<!-- 不要使用正数 tabindex → 破坏自然顺序 -->
焦点管理原则:
- 模态框打开时,焦点锁定在模态框内(
focus trap) - 模态框关闭时,焦点返回触发元素
- 单页路由切换时,焦点移至新内容顶部(
skip to content) - 使用
:focus-visible替代:focus(在有键盘操作时显示焦点环)
焦点管理示例
// 模态框焦点陷阱
class Modal {
constructor(triggerEl) {
this.triggerEl = triggerEl;
this.modal = document.getElementById('modal');
this.focusableEls = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
this.firstFocusable = this.focusableEls[0];
this.lastFocusable = this.focusableEls[this.focusableEls.length - 1];
}
open() {
this.modal.style.display = 'block';
this.firstFocusable.focus();
this.modal.addEventListener('keydown', this._trapFocus);
}
close() {
this.modal.style.display = 'none';
this.triggerEl.focus(); // 焦点回归
this.modal.removeEventListener('keydown', this._trapFocus);
}
_trapFocus = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
性能相关
defer vs async
<!-- 默认:解析遇到则阻塞下载+执行 -->
<script src="normal.js"></script>
<!-- async:下载不阻塞,下载完成后立即执行(执行可能阻塞) -->
<!-- 执行顺序与 DOM 位置无关,多个 async 执行顺序不确定 -->
<script async src="analytics.js"></script>
<!-- defer:下载不阻塞,DOMContentLoaded 之前按顺序执行 -->
<script defer src="app.js"></script>
<script defer src="dep.js"></script> <!-- 在 app.js 之前执行 -->
| 特性 | 普通 | async | defer |
|---|---|---|---|
| 下载阻塞解析 | 是 | 否 | 否 |
| 执行阻塞解析 | 是 | 是 | 否 |
| 执行时机 | 立即 | 下载完立即 | DOMContentLoaded 前 |
| 执行顺序 | 按文档顺序 | 不确定 | 按文档顺序 |
| DOMContentLoaded 等待 | 是 | 否 | 是 |
| 适用场景 | — | 独立第三方脚本 | 依赖 DOM 的脚本 |
preload / prefetch / dns-prefetch
<!-- preload:当前页面必需的资源,高优先级,尽早加载 -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="style.css" as="style">
<!-- prefetch:下一个页面可能需要的资源,低优先级,空闲时加载 -->
<link rel="prefetch" href="/next-page.js" as="script">
<!-- dns-prefetch:提前解析 DNS,减少跨域请求延迟 -->
<link rel="dns-prefetch" href="//api.example.com">
<!-- preconnect:DNS + TCP + TLS 握手 -->
<link rel="preconnect" href="//cdn.example.com">
<!-- 优先级:preload > preconnect > dns-prefetch > prefetch -->
注意:preload 需要配合 as 属性指定资源类型,否则浏览器无法确定优先级。
关键渲染路径中的 HTML 优化
关键渲染路径(Critical Rendering Path):浏览器从 HTML 到屏幕像素的步骤。
HTML → DOM 树 ┐
→ 渲染树 → 布局 → 绘制 → 合成
CSS → CSSOM 树 ┘
优化策略:
- 减少阻塞:内联关键 CSS(Critical CSS),首屏 CSS
<link>放<head>,非关键 CSS 异步加载 - 压缩资源:HTML/CSS/JS 压缩,移除注释空格
- 渐进式加载:图片懒加载
<img loading="lazy"> - 减少 DOM 深度:清除冗余嵌套减少渲染树节点
- 避免布局抖动:批量 DOM 操作、使用
requestAnimationFrame读布局:
// ❌ 强制同步布局(每次读 offsetHeight 都会触发重新计算)
element.style.width = '100px';
console.log(element.offsetHeight); // 强制回流
element.style.height = '200px';
// ✅ 先批量读,再批量写
const height = element.offsetHeight; // 读
requestAnimationFrame(() => {
element.style.width = '100px'; // 写
element.style.height = '200px';
});
- 长任务分解:超过 50ms 的任务会导致交互延迟,使用
requestIdleCallback分片:
function processLargeArray(arr) {
const chunkSize = 50;
let index = 0;
function doChunk() {
const end = Math.min(index + chunkSize, arr.length);
for (; index < end; index++) {
processItem(arr[index]);
}
if (index < arr.length) {
requestIdleCallback(doChunk, { timeout: 100 });
}
}
doChunk();
}