读完这篇,你能做什么
能让页面响应事件,并区分网络异常、HTTP 错误与 JSON 解析失败。
学习理由、准备要求与知识点
为什么学它
掌握最小浏览器脚本能力,才能独立观察交互、请求和异步结果。
先读这些内容
知识点清单
JavaScript与 Java 区别- let
/const绑定 - DOM 查询
textContent安全文本- 事件监听回调
- Promise 与 async
/await - fetch 状态和解析
- 重复点击和失败恢复
运行前先确认交互机制依据浏览器 DOM、HTML 与 Fetch 标准;JavaScript 与 Java 是不同语言。Java 源码选 OpenJDK 17u jdk-17.0.16+8 的 CompletableFuture 完成阶段,作为跨语言对照;Java 17 算法离线模拟过时响应过滤,不声称实现浏览器事件循环。
01先弄懂它是什么
点击按钮后页面立刻变化,是浏览器运行了监听这个事件的 JavaScript 函数,不是 Java 服务端直接改屏幕。let 变量可以重新赋值,const 不允许重新绑定但不会冻结对象内部。DOM 提供找节点和改文字的方法;普通用户文本应使用 textContent,让它保持为文字,不被重新当成 HTML 解析。
异步表示结果稍后才能得到,等待期间浏览器还能处理其他可运行任务。async 函数返回 Promise,await 暂停的是这个函数后续依赖结果的部分,不是整个页面。不过长时间的同步计算仍会占住主线程;只在函数前加 async,并不会自动把计算移到后台线程。
以搜索框为例,用户先输入 A,再输入 AB。第二次请求可能先返回,而第一次稍后才返回;如果每个成功响应都更新 DOM,页面会被旧结果覆盖。这里的问题不是请求是否成功,而是结果是否仍属于用户最新意图。需要把请求身份、完成状态与界面可见状态分开,再规定谁有权更新当前页面。
文中这几个词,先认一认
- 事件监听器
- 提前注册、等对应事件发生时由浏览器调用的函数,例如点击按钮后运行。
- Promise
- 代表未来结果的对象,之后可能成功,也可能失败;具体何时执行后续处理由运行环境调度。
- 事件循环
- 浏览器协调任务、微任务和绘制时机的机制,不表示每遇到 await 就新建线程。
- 过时响应
- 响应内容本身可能正确,但用户已发出更新的请求,所以它不应再覆盖当前页面。
02看看它怎么工作
每次输入变化时,先读取搜索词,把本次请求编号加一,再显示加载中并发出 fetch;这个递增编号就是这里说的代次。请求发出后,监听函数可以结束,用户继续输入又会得到新编号。响应回来时,把它出发时记下的编号与当前编号比较。只有两者相同,才允许显示结果或结束加载;旧请求无论成功还是失败,都不能改掉新请求的界面状态。
fetch 在拿到 HTTP 响应后通常会正常完成 Promise,即使状态是 404 或 500。因此先检查 response.ok 或具体状态,再按契约解析正文;网络错误、非成功 HTTP 状态、JSON 解析错误和业务拒绝应分层处理。只写 catch 无法自动覆盖所有服务端错误,把所有错误都显示为网络故障也会误导排查。
事件监听器可以先更新 loading,再在异步阶段继续执行,但界面真正何时绘制由浏览器决定。回调里的连续同步工作仍会阻塞其他任务;大量数据整理可拆分、限量或交给合适的 Worker。AbortController 可以中止客户端等待或请求处理的一部分,但服务端是否已执行写入不能只靠浏览器取消结果判断。
先跑通一个最小例子
在空目录保存下面两个文件,直接打开 index.html。脚本通过 defer 等文档解析完成后执行;用一个本地 Promise 模拟异步响应,先观察“处理中”,再观察完成结果。它不向真实服务器发送请求。
<!doctype html>
<html lang="zh-CN">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>学习主题表单</title>
<script src="app.js" defer></script>
<main>
<h1>记录学习主题</h1>
<form id="study-form">
<label for="topic">主题</label>
<input id="topic" name="topic" required maxlength="80">
<button type="submit">预览结果</button>
</form>
<p id="result" role="status">等待输入</p>
</main>
</html>const form = document.querySelector('#study-form');
const field = document.querySelector('#topic');
const result = document.querySelector('#result');
const button = form.querySelector('button');
let pending = false;
form.addEventListener('submit', async (event) => {
event.preventDefault(); // 接管表单提交,避免默认整页导航。
if (pending) return; // 同一次异步处理未完成时不重复启动。
const title = field.value.trim();
if (!title) { result.textContent = '请输入非空主题'; return; }
pending = true;
button.disabled = true;
result.textContent = '处理中…';
try {
const message = await new Promise(resolve => {
setTimeout(() => resolve(`准备学习:${title}`), 500);
});
result.textContent = message; // 当作文本显示,用户内容不进入 HTML 解析器。
} catch {
result.textContent = '处理失败,请重试';
} finally {
pending = false;
button.disabled = false; // 成功或失败都恢复交互状态。
}
});核对运行结果输入 Java 并提交,会先显示“处理中…”,约半秒后显示“准备学习:Java”。只输入空格时提示非空主题。输入 <b>Java</b> 会作为普通文本显示,不创建粗体节点。模拟完成不表示内容已经保存到数据库。
正在加载流程图…
查看流程图源码(Mermaid)
sequenceDiagram
participant U as 用户
participant P as 页面状态
participant S as 服务端
U->>P: 输入 A,生成代次 1
P->>S: 请求 1
U->>P: 输入 AB,生成代次 2
P->>S: 请求 2
S-->>P: 响应 2
P->>P: 当前代次为 2,展示新结果
S-->>P: 响应 1
P->>P: 代次过旧,忽略界面写入按顺序看,每一步做了什么
- 注册事件函数,读取 DOM 输入并生成稳定的本次代次。
- 发起异步请求,明确加载、成功与失败状态。
- 收到响应后先检查 HTTP 层,再解析业务内容。
- 提交界面更新前核对代次,旧成功和旧失败都不能覆盖新请求。
03对照实现代码
OpenJDK 17u,jdk-17.0.16+8 · java.util.concurrent.CompletableFuture#uniApplyStage、UniApply#tryFire
真实文件和方法已核验。下面重新编写一个完成状态转换模型,仅保留等待、成功、失败传播与转换函数异常四种分支;未实现并发、依赖注册、执行器或 Promise 微任务调度。Java CompletableFuture 与浏览器 Promise 的执行规则不能直接等同。
import java.util.function.Function;
class SourceModel {
record Outcome<T>(boolean done, T value, Throwable error) {}
static <T,R> Outcome<R> transform(Outcome<T> input, Function<T,R> fn) {
if (!input.done()) return new Outcome<>(false,null,null); // 前序未完成,不能计算
if (input.error()!=null) return new Outcome<>(true,null,input.error()); // 失败继续传播
try {
R value=fn.apply(input.value()); // 只有成功值才进入普通转换函数
return new Outcome<>(true,value,null);
} catch (RuntimeException failure) {
return new Outcome<>(true,null,failure); // 转换本身也可能失败
}
}
}thenApply 进入 uniApplyStage,真实实现会根据前序是否已完成决定立即处理或安排依赖动作。UniApply.tryFire 首先检查源结果及相关引用,条件不足时不会凭空执行转换。这对应异步组合的核心约束:依赖结果的计算必须在状态可用后才推进。
当源结果表达失败时,普通成功转换函数不会被当作恢复函数调用,失败会进入后续阶段。若源成功但转换函数抛异常,后续阶段同样失败。教学模型显式区分这两条来源,帮助解释解析 JSON 出错与网络请求失败为何可能最后都进入错误处理,但诊断原因不同。
Java 的非 Async 与 Async API 是否指定执行器,会影响动作在哪个执行线程执行;浏览器 Promise 反应按它自己的任务和微任务规则调度。因此这段源码用于对照完成依赖和失败传播,不用于证明 JavaScript 的 then 会像某个 Java 方法一样在调用者线程立刻运行。
异步阶段成功,只表示这次计算有了结果,不知道它是否仍适合当前页面。过滤旧搜索结果,要另外记录请求代次或管理取消。把“请求成功”与“这个结果还能不能更新界面”分开判断,才能解决并未抛异常的乱序覆盖。
代码先调用谁,再调用谁
- 调用
thenApply表达结果转换依赖。 uniApplyStage判断前序状态并建立后续阶段。UniApply.tryFire在条件满足后传播失败或执行函数。- 后续阶段完成,调用方仍需判断结果是否适用于当前业务状态。
04动手写一个例子
最新请求获准更新界面的代次状态机
顺序接收 start 与 complete 事件。start 生成递增请求 ID 并进入 loading;只有最新 ID 的第一次完成能更新界面,旧响应和重复完成被忽略,未发起的 ID 被拒绝。模拟单个页面组件的单线程事件处理,不涉及网络或多线程竞争。
- 输入 1
start1,start2,success2(B),success1(A)- 输出 1
data:B
- 输入 2
start1,start2,error1(E)- 输出 2
loading
- 输入 3
start1,error1(E)- 输出 3
error:E
- 输入 4
无事件- 输出 4
idle
当前界面只关心最新一次意图,因此不必保存全部历史响应。维护 latest、finished 和 view 三个状态,开始新请求时递增 latest、清除 finished 并进入 loading。
完成事件先验证 ID 属于已发起范围。旧 ID 无权更新当前页面;等于 latest 但已经完成,也应忽略,防止重复回调覆盖第一次结果。只有最新且尚未完成的事件能原子地改变本模型的状态。
这里的原子性来自按顺序处理事件的题目前提,不是把这些 Java 字段直接放进多线程就安全。main 自测正常完成、倒序返回、旧失败、最新失败、重复回调、空状态和无效 ID;计数溢出另行拒绝。
为什么这样写是对的view 的已完成内容只能来自 latest 对应请求的第一次完成;开始新请求后任何旧 ID 都不能改变 loading 或新结果。
状态机只保存最新代次、完成标志和当前显示值;不保存所有在途请求。字符串内容的分配成本另按其长度计算。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
public class Main {
static class Gate {
long latest=0;
boolean finished=false;
String view="idle";
long start() {
latest=Math.incrementExact(latest); // 溢出时拒绝,不能重复使用旧代次
finished=false;
view="loading";
return latest;
}
void complete(long id,String value,boolean error) {
if(id<=0 || id>latest || value==null) throw new IllegalArgumentException();
if(id!=latest || finished) return; // 旧响应或重复响应都不能覆盖页面
finished=true;
view=(error?"error:":"data:")+value; // 此题单线程处理检查与写入
}
}
static void eq(String expected,String actual) {
if(!expected.equals(actual)) throw new AssertionError(actual);
}
public static void main(String[] args) {
Gate empty=new Gate(); eq("idle",empty.view);
Gate normal=new Gate(); normal.complete(normal.start(),"A",false); eq("data:A",normal.view);
Gate reverse=new Gate(); long old=reverse.start(),fresh=reverse.start();
reverse.complete(fresh,"B",false); reverse.complete(old,"A",false); eq("data:B",reverse.view);
Gate staleError=new Gate(); long one=staleError.start(); staleError.start();
staleError.complete(one,"offline",true); eq("loading",staleError.view);
Gate failed=new Gate(); failed.complete(failed.start(),"bad",true); eq("error:bad",failed.view);
Gate duplicate=new Gate(); long id=duplicate.start();
duplicate.complete(id,"first",false); duplicate.complete(id,"second",false); eq("data:first",duplicate.view);
try { new Gate().complete(1,"x",false); throw new AssertionError("应拒绝未发起 ID"); }
catch(IllegalArgumentException expected) { /* 无效输入已拒绝 */ }
Gate overflow=new Gate(); overflow.latest=Long.MAX_VALUE;
try { overflow.start(); throw new AssertionError("应拒绝代次溢出"); }
catch(ArithmeticException expected) { /* 极值边界已验证 */ }
System.out.println("PASS 8 request gate tests");
}
}这些特殊情况也要试一试
- 最新请求失败也应结束它自己的 loading,不能只允许成功更新。
- 旧请求失败不应把新请求页面改成错误态。
- 组件销毁后还需独立的生命周期检查;代次不能自动证明组件仍挂载。
进阶选读 · 生产拓展:多分页与局部结果的版本管理
场景搜索支持分页、自动补全和局部缓存更新,仅用一个全局最新代次会错误丢弃仍有效的分页结果。
做法按查询身份、分页游标和组件生命周期分层标记版本,定义哪些结果可合并、哪些必须替换。取消用于节约资源,版本校验仍守住提交结果的边界。
复杂度单个结果校验可保持 O(1) 查表,但需要 O(q+p) 保存活跃查询与分页状态;合并排序结果的成本取决于具体数据结构。
好处与代价细粒度版本提高并行利用率,却增加状态清理与一致性规则;全局代次简单,但只适合整块结果替换。
05项目里怎么用
快速重复点击可在页面禁用按钮或合并请求,但写操作仍需要服务端幂等和资源权限。浏览器超时或取消不保证服务端没有执行,不能把重新启用按钮直接等同于允许再次扣款。读取搜索可以忽略旧结果,订单提交则要用稳定业务标识查询状态。
错误展示应区分需要重试、重新登录、修正输入或联系支持的情况。日志记录请求关联标识和阶段,不记录完整凭据。对于 fetch,先处理响应状态再解析正文,避免 HTML 错误页引发 JSON 异常后只显示一个无法定位的提示。
加载和错误状态也要让键盘用户和辅助技术知道:按钮是否在等待、结果是否已更新、失败后怎样重试,都需要明确表达。大列表要控制一次同步处理的工作量,必要时分批显示或安排合适后台计算。看输入延迟和长任务记录,比给所有函数加 async 更能找到卡顿原因。
容易踩的坑
- 注册监听器时误把函数调用结果传入,导致行为立即执行或监听器缺失。
- 认为 fetch 对所有 4xx
/5xx都会自动进入 catch。 - 把 await 当作冻结整个页面或自动新建线程。
- 只过滤旧成功而遗漏旧失败,仍会发生过时状态覆盖。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 忽略过时代次 | 简单地守住页面最新意图。 | 旧请求可能继续消耗网络与服务端资源。 | 搜索、详情切换等整块读取结果替换。 |
| 客户端取消加版本校验 | 兼顾资源节约和最终界面正确性。 | 取消存在时序边界,不能撤销所有服务端副作用。 | 频繁输入或组件切换。 |
| 合并与缓存请求 | 减少相同请求的重复工作。 | 需要定义缓存键、有效期和多调用方生命周期。 | 多组件读取相同稳定资源。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01await 为什么不等于后台线程?
展开参考答案与推导
它表达当前异步函数在结果可用后继续执行,具体工作由宿主 API 与运行时安排。连续同步计算仍可占住主线程,不能靠 async 关键字自动迁移。
02成功响应为何也可能不应该更新页面?
展开参考答案与推导
响应可能属于旧查询意图。传输成功与结果仍适用于当前界面是不同条件,需要请求身份或版本校验。
03Java CompletableFuture 能否证明浏览器 Promise 的调度细节?
展开参考答案与推导
不能。两者可用于比较依赖完成与异常传播,但执行线程、微任务和宿主事件循环规则不同,应分别查对应规范。
换个情况,还会处理吗
01先搜索 A 后搜索 AB,A 最后返回覆盖结果,怎样修复?
展开参考答案与推导
发起时保存查询代次,所有成功和失败处理在更新界面前检查是否仍为最新;取消旧请求可节省资源,但不代替提交前校验。
02服务返回 500,代码没有进入 catch,是否 fetch 故障?
展开参考答案与推导
通常不是。拿到 HTTP 响应可使 fetch 正常完成,需检查 ok 或状态,再决定是否抛业务错误或展示失败;同时区分正文解析失败。
03点击后页面卡两秒,即使函数是 async,怎么看?
展开参考答案与推导
检查 await 前后同步计算和 DOM 操作的长任务,测量主线程占用;拆分工作或采用合适的 Worker,而不是继续增加无意义的 await。
写代码试一试
01给代次状态机增加组件销毁事件。
- 输入 / 条件
start1,destroy,success1(A)- 输出 / 预期
不更新已销毁组件
提示 有效代次与组件存活是两个条件。
展开参考答案与推导
增加 active 标志或生命周期代次,销毁时失效;完成事件同时验证组件身份与请求代次,重新挂载不能沿用旧授权。
02怎样验证旧失败不会结束新 loading?
- 输入 / 条件
start1,start2,error1(E)- 输出 / 预期
loading
提示 错误分支也使用统一门控。
展开参考答案与推导
在错误路径先比较请求代次,不直接修改 loading 或 error 字段;给它写独立自测,避免仅成功路径正确。
03为 fetch 处理流程划分错误类型。
- 输入 / 条件
网络拒绝、HTTP 404、无效 JSON- 输出 / 预期
网络错误、资源响应错误、解析错误
提示 各阶段分别检查。
展开参考答案与推导
先等待 fetch,检查 HTTP 状态,再按媒体类型解析内容;捕获时保留阶段信息,必要时转成统一但可诊断的错误结构。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能让页面响应事件,并区分网络异常、HTTP 错误与 JSON 解析失败。
本篇重点加练
重点加练异步状态与用户重复操作,检验请求生命周期和界面生命周期是否一致。
- Promise 与 async
/await - 重复点击和失败恢复
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
给出 let 计数、const 对象与 Java 局部变量的独立样例,比较重新赋值、对象字段修改及运行入口;不得仅按语法相似推断两种语言机制。
- 提交内容
- 语言与绑定行为对照、最小运行记录。
- 怎样算完成
- 绑定和对象内容分开
- 结论由各自运行环境验证
练到的知识点JavaScript 与 Java 区别 · let/const 绑定
- 作业 02编程实践
创建一个本地页面,按钮读取输入文本并用
textContent显示;输入含 <b>课程</b>、空串和中文,给每次点击记录序号,不使用innerHTML处理用户文本。- 提交内容
- HTML
/JS、自写输入和重复点击测试、浏览器证据。 - 怎样算完成
- DOM 查询失败有明确处理
- 输入按文本合同展示
- 监听不会重复注册累积
练到的知识点DOM 查询 · textContent 安全文本 · 事件监听回调
综合应用
- 作业 03代码推演
用 Promise 和
setTimeout模拟两次异步加载,设延迟分别为 80ms 与 10ms;先写出可确定与不可确定的执行关系,再记录 await 前后事件。- 提交内容
- 异步时间线和实际事件记录。
- 怎样算完成
- 调用顺序与完成顺序分开
- 不将 await 理解成独立工作线程
练到的知识点Promise 与 async/await
- 作业 04排错实验
使用本地模拟请求函数分别返回成功 JSON、HTTP 错误、非法 JSON 和拒绝的 Promise;检验页面加载状态、错误提示和下一次点击恢复。
- 提交内容
- 页面代码、自写四类失败与边界测试、运行证据。
- 怎样算完成
- 状态错误与解析错误分开
- 失败后可继续操作
- 旧请求不会随意覆盖新状态
练到的知识点fetch 状态和解析 · 重复点击和失败恢复
- 作业 05面试场景
搜索框先输入 Java 后输入 JVM,第一次请求较慢;组件在响应前又被移除。为最新结果、取消和销毁三个事件建立页面更新合同。
- 提交内容
- 事件序列、状态图与验收案例。
- 怎样算完成
- 更新资格由合同确定
- 取消与完成竞态都被考虑
练到的知识点DOM 查询 · 事件监听回调 · Promise 与 async/await · 重复点击和失败恢复
- 作业 06编程实践
实现本地模拟“保存笔记”按钮,保存中按合同禁用重复提交,失败后允许重试;使用可控 Promise 注入先失败后成功,不发送真实请求。
- 提交内容
- HTML
/JS、自写连续点击/失败恢复/完成测试及运行证据。 - 怎样算完成
- 按钮状态对应实际任务
- 重试产生的操作次数可查
练到的知识点Promise 与 async/await · 重复点击和失败恢复
- 作业 07代码推演
两个 Promise 中一个成功一个失败,程序需要同时等待并展示各自结果。写出按顺序 await 与共同等待时的观察合同,注明错误在哪一层处理。
- 提交内容
- 执行关系图和可控样例记录。
- 怎样算完成
- 完成顺序与展示次序分开
- 失败不会被未定义地吞掉
练到的知识点Promise 与 async/await
- 作业 08设计评审
用户保存过程中编辑了表单,保存响应随后返回。定义当前编辑内容、已保存版本和成功提示的关系,列出响应更新页面的必要条件。
- 提交内容
- 版本状态图和交互验收序列。
- 怎样算完成
- 编辑状态与请求快照分开
- 提示含义可由数据证据验证
练到的知识点重复点击和失败恢复
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成状态机后:实现带明确重试按钮的异步资料面板,最多允许一个最新请求更新视图;用可控 Promise 测试乱序成功、旧失败及卸载后完成。
- 提交内容
JavaScript实现、自写确定性事件测试和浏览器运行证据。- 怎样算完成
- 不依赖真实网络时序
- 重试不会造成监听或状态泄露
- 界面状态和请求生命周期可追踪
练到的知识点事件监听回调 · Promise 与 async/await · fetch 状态和解析 · 重复点击和失败恢复
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
保存功能增加“用户离线时排队,恢复后提交”。重新评审重复点击、旧响应与重试的语义,暂不实现浏览器离线存储。
- 提交内容
- 事件序列与提交合同。
- 怎样算完成
- 每次编辑的身份可追踪
- 联网前后的成功判据明确
练到的知识点Promise 与 async/await · 重复点击和失败恢复
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程中文
MDN:JavaScript 指南 (新标签页)↗
按顺序阅读变量、函数、对象和 Promise;注意
JavaScript与 Java 的类型差异。 - 实践实践指南中文
MDN:使用 Fetch (新标签页)↗
练习构造请求、检查
response.ok、解析 JSON,并理解跨源请求和异常行为。 - 深入教程中文
MDN:HTML 结构基础 (新标签页)↗
学习语义元素、标题、链接与表单结构,先让页面可读且可操作。
- 深入参考文档英文
WHATWG HTML Event Loops (新标签页)↗
阅读任务、微任务检查点与浏览器执行模型。
- 深入参考文档英文
Fetch Standard (新标签页)↗
核对网络失败和 HTTP 响应状态的不同结果。
- 深入参考文档英文
DOM Standard (新标签页)↗
阅读事件监听、分发和文本节点操作。
- 深入参考文档英文
OpenJDK 17u CompletableFuture (新标签页)↗
定位
uniApplyStage与UniApply.tryFire的前序检查及失败传播。