Webapi:XMLHttpRequest是怎么实现的|浏览器篇
# Webapi:XMLHttpRequest是怎么实现的
30 秒速记
XMLHttpRequest让页面通过接口获取服务器数据,再局部修改DOM,无需因一条数据变化而刷新整个页面。- 它把 JavaScript、浏览器网络能力和页面更新连接起来,是理解浏览器
WebAPI工作方式的典型入口。 - 网络操作不能只按普通函数调用理解:请求结果需要通过回调在后续时机交还给页面逻辑。
- 理解其机制前要区分同步回调与异步回调,因为两者进入执行流程的时间和所处任务不同。
- 该能力提高了局部更新效率,但请求过程还会涉及
HTTP、浏览器进程协作及安全限制。
XMLHttpRequest 本质上是浏览器提供给 JavaScript 的网络能力,用来异步请求服务器数据并局部更新页面。 调用 open 和 send 后,请求交给浏览器的网络模块处理,当前调用栈不会一直等待。响应到达后,状态变化会形成后续任务,事件循环再触发 load、error 等回调,业务代码随后解析数据并修改 DOM。工程中还要检查 status 和响应内容,因为 404 通常也会进入 load,而网络失败、超时和 CORS 限制需要分别处理。
在上一篇文章中我们介绍了 setTimeout 是如何结合渲染进程的循环系统工作的,那本篇文章我们就继续介绍另外一种类型的 WebAPI——XMLHttpRequest。
自从网页中引入了 JavaScript,我们就可以操作 DOM 树中任意一个节点,例如隐藏 / 显示节点、改变颜色、获得或改变文本内容、为元素添加事件响应函数等等, 几乎可以“为所欲为”了。
不过在 XMLHttpRequest 出现之前,如果服务器数据有更新,依然需要重新刷新整个页面。而 XMLHttpRequest 提供了从 Web 服务器获取数据的能力,如果你想要更新某条数据,只需要通过 XMLHttpRequest 请求服务器提供的接口,就可以获取到服务器的数据,然后再操作 DOM 来更新页面内容,整个过程只需要更新网页的一部分就可以了,而不用像之前那样还得刷新整个页面,这样既有效率又不会打扰到用户。
关于 XMLHttpRequest,本来我是想一带而过的,后来发现这个 WebAPI 用于教学非常好。首先前面讲了那么网络内容,现在可以通过它把 HTTP 协议实践一遍;其次,XMLHttpRequest 是一个非常典型的 WebAPI,通过它来讲解浏览器是如何实现 WebAPI 的很合适,这对于你理解其他 WebAPI 也有非常大的帮助,同时在这个过程中我们还可以把一些安全问题给串起来。
但在深入讲解 XMLHttpRequest 之前,我们得先介绍下同步回调和异步回调这两个概念,这会帮助你更加深刻地理解 WebAPI 是怎么工作的
原理拆解: XMLHttpRequest 是渲染进程中暴露给 JavaScript 的浏览器能力。调用 open 和 send 后,请求交由浏览器网络相关模块处理;异步请求不会让当前 JavaScript 调用栈一直等待。响应状态变化会在后续时机形成任务,事件循环取出任务后调用 readystatechange、load、error 等监听器,页面逻辑再解析数据并修改 DOM。因此“发出请求”和“消费结果”分属不同执行时段。
最小验证: 输入是公开测试接口返回的 JSON;要验证的结论是异步请求先让当前脚本结束,随后才执行 load 回调。可在现代浏览器控制台直接运行:
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/todos/1", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
console.log("response", xhr.response.title);
} else {
console.log("http error", xhr.status);
}
});
xhr.addEventListener("error", () => {
console.log("network error");
});
console.log("before send");
xhr.send();
console.log("after send");
关键参数 true 表示异步模式。预期先输出 before send、after send,请求成功后再输出 response。若离线、DNS 或连接失败会触发 error;服务器返回 404 通常仍进入 load,所以必须检查 status。若目标服务调整了跨域策略,浏览器也可能因 CORS 拒绝把响应交给脚本。
边界与排查: 同步 XMLHttpRequest 会阻塞执行线程,在主线程上会造成页面无法响应,现代工程不应依赖它。请求成功也不代表业务数据有效,仍需校验状态码、响应类型和字段。取消请求可使用 abort(),超时可设置 timeout 并监听 timeout。排查时结合网络面板检查请求头、响应头、状态码、耗时和预检请求,再用性能面板确认回调中是否存在大规模 DOM 更新或长任务;网络完成很快但界面仍卡顿时,瓶颈往往位于响应处理与渲染阶段。
面试官追问
追问 1库存页调用异步 xhr.send() 后,下一行立即读取 xhr.response.title,评审者认为局域网足够快所以不会为空,你怎么反驳?
网络快也不能把异步请求变成同步执行,当前脚本会先继续运行,响应只能在后续的 load 等回调中消费。最小验证应先看到 before send、after send,再看到响应;立即读取存在时序错误。
追问 2商品列表每五秒刷新一次库存,只允许更新对应数字而不能整页闪烁,你会让 XMLHttpRequest 和页面代码分别承担什么职责?
XMLHttpRequest 负责请求库存接口,并在异步回调中交付响应;页面代码校验结果后只修改对应的 DOM 节点。局部请求避免整页重载,但回调若批量执行大规模 DOM 更新,界面仍可能出现长任务和卡顿。
追问 3公开接口返回 404 时仍触发了 load,开发者因此把它当成请求成功并渲染 xhr.response,你会怎样修正?
load 表示传输流程已经完成,不保证 HTTP 状态代表成功,404 通常也会进入该回调。应在其中检查 status 的成功区间,并继续校验响应类型和业务字段;网络失败才主要由 error 处理。
追问 4线上请求在开发机正常、用户浏览器却失败,控制台同时可能出现离线、DNS、连接或 CORS 信息,你会怎样缩小范围?
先在网络面板核对请求是否发出、状态码、耗时、请求头、响应头和预检请求,再区分 error、timeout 与 HTTP 错误。若服务端有响应但脚本拿不到数据,应继续检查 CORS;不能只盯着页面更新代码。
追问 5搜索联想框连续输入会产生多次未完成请求,团队在“等待全部返回”和“取消旧请求”之间争论,你会如何选择?
若旧结果已无展示价值,可对过期的 XMLHttpRequest 调用 abort(),并只消费当前查询对应的响应。这样能减少陈旧结果覆盖新结果的机会,但仍需在回调层校验请求身份;取消请求不能替代状态码和数据校验。
追问 6接口在网络面板中很快完成,但包含数百条数据的页面仍明显卡顿,你会把排查重点放在哪里?
应转向响应解析、回调逻辑和渲染阶段,用性能面板检查是否出现大规模 DOM 更新或长任务。异步请求只是不让当前调用栈等待网络,并不保证后续处理轻量;数据量增大时,瓶颈可能完全位于主线程。
# 回调函数 VS 系统调用栈
30 秒速记
- 回调函数是作为参数交给另一个函数、并由后者在约定时机调用的函数。
- 同步回调在主函数返回前执行,因此仍处于当前任务及其调用上下文中。
- 异步回调在主函数结束后执行,不继续占用原主函数的 JavaScript 调用栈。
- 页面主线程按消息队列逐个处理任务;异步回调可以被封装为新任务追加到队列,也可以进入微任务队列。
- 微任务在当前任务末尾处理,而普通异步任务需要等待主线程从消息队列取到它;两者的执行时机不同。
- 浏览器还为每个任务维护由 Chromium 的
C++代码形成的系统调用栈,可通过Performance或chrome://tracing/观察相关过程。
回调函数是作为参数传入其他函数,并在约定时机被调用的函数;系统调用栈则是浏览器执行任务时由 Chromium 的 C++ 代码维护的调用链。 同步回调在主函数返回前执行,仍处于当前任务和调用上下文;异步回调在主函数结束后执行,不再占用原来的 JavaScript 调用栈。异步回调既可能作为新任务进入消息队列,也可能进入微任务队列,在当前任务末尾执行。排查执行过程时,可以通过 Performance 或 chrome://tracing/ 观察任务及其内部子过程。
