JavaScript:DOM 与异步交互
从语言走向浏览器
先完成 JavaScript 基础,包括函数、类与模块,再结合 HTML、CSS 学习本篇。DOM 操作、事件与 Fetch 由浏览器提供;Promise 和 async/await 用于组织异步结果,不需要先学框架。
把以下代码保存为 HTML 后打开,输入名字并点击按钮:
<!doctype html>
<html lang="zh-CN">
<head><meta charset="utf-8"><title>问候</title></head>
<body>
<label for="name">名字</label>
<input id="name">
<button id="greet" type="button">问候</button>
<p id="message" aria-live="polite"></p>
<script type="module">
const input = document.querySelector("#name");
const message = document.querySelector("#message");
document.querySelector("#greet").addEventListener("click", () => {
message.textContent = `Hello, ${input.value.trim() || "visitor"}!`;
});
</script>
</body>
</html>查询可能返回 null,通用代码需检查元素是否存在。输出文本使用 textContent;innerHTML 会解析标记,不能直接插入不可信输入。不要使用 document.write 作为普通页面更新方式。
事件传播与清理
事件可能经过捕获、目标、冒泡阶段,并非所有事件都冒泡。event.target 是触发来源,event.currentTarget 是当前监听器绑定的元素。preventDefault() 阻止可取消的默认行为,stopPropagation() 阻止继续传播,两者不同。
事件委托把监听器放在稳定父节点,通过 closest 识别实际操作按钮,兼容按钮内还含有图标的情况:
// 前提:页面已有 #tasks,内部按钮使用 data-id
const tasks = document.querySelector("#tasks");
function handleClick(event) {
const button = event.target instanceof Element
? event.target.closest("button[data-id]") : null;
if (!button || !tasks.contains(button)) return;
console.log(button.dataset.id);
}
// 在 tasks 确实存在时注册
if (tasks) tasks.addEventListener("click", handleClick);
// 拆除这部分界面时,用同一函数引用清理
if (tasks) tasks.removeEventListener("click", handleClick);最后一行演示拆除时的操作,实际应用应在界面生命周期结束时执行,不能注册后立即清理。定时器也需要保存 ID 后用 clearTimeout/clearInterval 取消。
Promise、await 与事件循环
Promise 表示异步操作最终的成功或失败。async 函数总返回 Promise,await 暂停当前异步函数,并不阻塞整个线程,也不会自动把 CPU 密集计算放到后台。
console.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// 此独立示例依次输出 start、end、microtask、timer当前同步任务结束后处理已入队微任务,再进入后续任务;零延迟定时器不保证立即执行。不要把浏览器模型机械套用为 Node.js 所有调度细节。
const delay = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
async function loadBoth() {
const values = await Promise.all([delay("A", 10), delay("B", 1)]);
console.assert(values.join("") === "AB"); // 结果按输入顺序,而非完成顺序
return values;
}
loadBoth().catch(console.error);独立任务可并发启动;有数据依赖的任务要顺序等待。Promise.all 遇拒绝会拒绝,但不会自动取消其他任务;需要收集每个成功/失败结果时可考虑 allSettled。不要用 forEach(async ...) 期待外层自动等待,顺序处理用 for...of,并发处理用 Promise.all(items.map(...))。Promise 指南解释了组合方式。
Fetch 与失败处理
fetch 遇网络失败通常拒绝,但 HTTP 404/500 通常仍得到 Response,必须检查 ok 或状态码。响应体读取也是异步操作。
async function getJson(url, signal) {
const response = await fetch(url, { signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
// 需要一个返回 JSON 的同源 /api/items 服务
async function showItems() {
const controller = new AbortController();
try {
const items = await getJson("/api/items", controller.signal);
console.log(items);
} catch (error) {
console.error("读取失败", error);
}
}本段定义调用方式,不会自动发出请求。实际页面应在离开界面或切换查询时取消过时请求,并处理旧响应覆盖新状态的问题。跨源请求还受 CORS 和凭据策略限制。见 Fetch 指南。
调试与练习
在开发者工具中给事件处理器设断点,观察输入值;用 Network 面板区分 HTTP 失败和网络失败,再检查是否等待并捕获 Promise。运行调度示例确认顺序,给 getJson 增加 404 和无效 JSON 情况。框架学习从 Vue 或 React开始。
参考核对
输入首尾空白会被trim()移除,全空白回退为visitor;调度顺序为start、end、microtask、timer。让读者自己的同源测试服务分别返回404和200但响应体为not JSON:前者在response.ok检查处抛HTTP 404,后者在response.json()拒绝为解析错误。网络断开则通常在fetch本身拒绝,三者都应由调用处的try/catch或Promise.catch处理;没有测试服务时这些只是预期,不能声称已请求验证。
深入课程已接续:任务所有权与取消。基础内容与旧锚点保留,按当前路线进入完整机制和实践。
继续阅读
- 排错
- 语言与交互
- 语言与交互