JavaScript 为什么会先显示加载状态
通过搜索请求解释同步执行、微任务与 await,并用请求身份处理加载终态和过期响应覆盖问题。
定义搜索状态转换,并验证空结果、拒绝与逆序完成的请求不会留下错误界面状态。
搜索按钮被点击后,页面先显示加载,结果稍后出现。这段等待不是 JavaScript 在一行代码里同时做所有事情:网络等平台能力在处理外部工作,完成后的回调再进入执行队列。界面能否及时反馈,取决于同步代码是否让出执行机会以及结果如何更新状态。
本课使用资料搜索案例解释任务、微任务、Promise 和过期结果。离线例子只列出简化队列顺序,不运行浏览器、网络或真实计时器,也不把 Python 的调度行为当成 JavaScript 行为。
同步执行先走完当前调用
在同一个 JavaScript 事件循环上,正在执行的同步代码通常运行到完成,其他回调不能任意插进当前调用栈。点击处理函数如果进行很长的纯计算,按钮、输入和绘制都可能等待。把函数声明为 async,不会自动把其中的计算送到其他线程。
平台能异步等待网络或计时器,再安排相应回调执行。定时器指定的延迟也不是精确的执行时刻:当前任务、其他排队任务与平台策略都会影响实际开始时间。不能凭一个零延迟定时器声称它立即执行。
Web Worker 等机制可以把合适的计算放到另一个执行环境,但涉及消息、数据传递与生命周期。普通资料搜索首先需要合理组织请求与状态;只有确认主线程计算造成阻塞,再评估拆分计算或工作线程。
Promise 回调和 await 的含义
Promise 表达未来的完成或失败。它的反应回调通过微任务调度;在典型浏览器事件循环中,当前同步任务结束后会处理微任务,再进入后续任务。不断添加微任务也会拖延其他工作,因此 Promise 并不是无限拆分计算的免费性能方案。
await 暂停当前 async 函数的后续执行,等待值完成,但调用者仍可以继续处理其他工作。await 前面的同步部分已经运行,后面继续执行仍需要调度。不要把暂停一个函数理解为暂停整个浏览器,也不要把它理解为自动并行。
对外部请求必须处理失败。网络连接失败、HTTP 错误和成功响应中的字段错误属于不同层;fetch 的 Promise 完成不代表 HTTP 状态一定成功。先检查请求结果,再按契约解析数据,最后更新 loading、success、empty 或 error 状态。
加载状态需要覆盖每条退出路径
点击搜索后先记录当前查询,再进入 loading。成功且有数据显示结果,成功但为空显示 empty,失败显示 error 并保留输入。若只在成功分支取消加载,拒绝路径会留下永不结束的 spinner。
清理逻辑通常适合处理结束动作,但不能让旧请求的清理覆盖新请求的 loading。连续查询时,每次请求需要自己的身份,只有当前有效请求可以修改当前界面。否则第一次失败的 finally 可能让第二次请求看起来已结束。
闭包捕获的是作用域中的绑定,某次异步回调使用哪个值要结合变量声明、赋值与调用时机判断。可以在发请求前保存本次查询与请求序号,避免回调从可变的全局输入里重新取值。不要笼统地说所有闭包都会保存一份独立数据副本。
用离线队列模型观察顺序
示例把同步输出 start 和 end 放在结果列表,再处理 promise 微任务,最后处理 timer 任务。数组只是合成队列;真实浏览器还包含渲染机会、多个任务来源、嵌套调度和更多平台规则。
tasks = ["timer"]
microtasks = ["promise"]
observed = ["start", "end"]
while microtasks:
observed.append(microtasks.pop(0))
while tasks:
observed.append(tasks.pop(0))
assert observed == ["start", "end", "promise", "timer"]
print(observed)
输出是 ['start', 'end', 'promise', 'timer']。断言验证这个模型选择的顺序,帮助解释为什么 Promise 回调不会打断当前同步段。它没有测量耗时,无法证明页面会在 promise 前还是后绘制,也不能证明所有浏览器任务共享一个简单 FIFO。
在真实浏览器做对应观察时,可以记录同步日志、一个已完成 Promise 的回调和定时器回调。把实际日志顺序与机制比较,但将计时器延迟和绘制单独观察。没有运行过浏览器实验,就不能把这个 Python 输出称为浏览器实测结果。
防止较旧结果覆盖最新查询
假设先查“索引”,随后查“事务”。第一请求晚返回时,如果直接赋值,页面会显示旧查询的资料。可以为每次查询增加序号,只允许与当前序号一致的结果提交状态;也可以用取消机制减少无用请求,再保留身份检查。
取消不意味着服务器一定停止了所有工作,也不保证已完成的回调从未进入队列。页面卸载、筛选变化与重试同样要让过期结果失效。请求 ID 应是应用控制的信息,不以返回顺序推断请求的新旧。
测试时让“旧慢、新快”与“旧快、新慢”都发生,并加入旧请求失败、新请求成功的情况。验收关注最终状态属于哪个查询,而不是哪个网络调用最先结束。存在版本化服务数据时,还需区分请求序号与服务端数据版本。
搜索框每次输入都发请求还可能造成无用负载。可以采用明确的提交动作,或按产品需要延迟触发;这种策略减少请求次数,却不代替过期结果检查。延迟触发时还要处理输入清空、离开页面和再次输入,避免旧计时回调发出已经无意义的查询。
请求身份与数据身份应分开记录。查询 ID 用来控制谁可以更新本次页面,资料 ID 用来确定结果代表哪条记录;把两者混用,会在重试或重复查询时产生错误判断。错误消息也应属于当前查询,不能沿用上一轮失败。
练习:设计可控的搜索状态机
写出 idle、loading、success、empty、error 的合法转换,定义查询 ID 和结果所属 ID。用可手动完成的教学请求替身模拟两次查询,不需要连接真实搜索服务。分别完成旧请求、新请求,观察每一步状态。
验收要求:空结果结束加载;失败保留查询并可重试;旧成功不覆盖新结果;旧失败不关闭新加载;组件离开后旧回调不再更新可见状态。再加入一段大同步计算,观察真实浏览器响应是否受阻,明确 async 声明无法改变这段计算的执行环境。记录顺序与行为,避免没有测量就给出性能倍数。
资料来源与延伸阅读
资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。