Ajax与fetch:前端异步请求原理与实战指南

发布时间:2026/10/20 5:36:24

Ajax与fetch:前端异步请求原理与实战指南 做前端开发Ajax差不多是跟后端打交道的第一道门槛。我记得自己刚接触那会儿觉得“发送请求、拿数据、渲染页面”这套流程挺绕的尤其被各种回调函数绕得晕头转向后来才慢慢理清楚。这篇就把Ajax从创建对象到拿到数据的完整流程讲透包括底层原理、实操步骤、常见问题以及如今更推荐的fetch和async/await方案争取让不同基础的读者都能在自己的项目里直接用起来。1. 内容整体设计与思路拆解1.1 Ajax的核心本质是什么Ajax全称是Asynchronous JavaScript and XML虽然名字里有XML但现在的实际应用中XML早已被JSON取代它代表的是一种“不刷新页面也能和后端交换数据”的能力。传统网页交互是点一个链接浏览器向服务器发请求服务器返回整个HTML页面浏览器整页刷新。想想现在的后台管理系统、移动端H5页面如果每次切换Tab、点一个按钮都整页刷新体感会非常糟糕。Ajax要解决的就是这个体验问题——它通过JavaScript在后台悄悄建立网络连接把服务端返回的数据交给你再由你用DOM操作局部更新页面。这里需要建立的一个核心认知是Ajax是一个“用代码模拟浏览器发请求”的工具。浏览器地址栏输入地址回车会发请求Ajax的代码也能发请求。区别在于前者会加载整个新页面后者拿回来的只是数据怎么用完全由你来决定。理解这个本质之后很多东西就能顺理成章地理解了为什么Ajax能局部刷新因为页面本身没有离开只是脚本拿到了响应数据。1.2 为什么用XMLHttpRequest这个对象Ajax的实现基础是浏览器内置的XMLHttpRequest对象缩写就是XHR。它包含一套完整的http请求处理机制准备阶段、发送阶段、等待响应阶段、数据接收阶段。浏览器把这个对象的定义内置好了开发者直接用就行不需要引入任何第三方库。XHR的设计理念比较“绕”——它认为一次请求要经过多个不同的阶段每个阶段对应readyState属性上不同的数字状态。请求过程中浏览器会不断触发onreadystatechange事件让你有机会在每个阶段做对应的处理。这是理解Ajax最核心的一条线请求不是一锤子买卖而是分阶段推进的我们需要监听到“响应到达且数据完整”那个阶段再去处理数据。这条线理清楚了整个Ajax学习就算打通了一半。剩下的一半是搞清楚状态码的含义以及各种异常情况的处理方式。2. 核心细节解析与实操要点2.1 XMLHttpRequest实例和它的五态变化创建一个XHR对象在老版本IE之外的环境里很简单const xhr new XMLHttpRequest();这一步完成之后这个对象身上有一个非常重要的属性readyState它的取值从0到4含义分别是readyState阶段说明0UNSENTXHR对象已创建但open方法还没调用此时请求完全没开始1OPENEDopen方法已调用请求地址和方式已经设置好2HEADERS_RECEIVEDsend方法已调用且已经收到服务器响应的头部信息3LOADING正在接收响应体数据但还没接收完整4DONE响应数据接收完成此时才能完整读取responseText可以用生活化类比来理解这个流程。你把open当成“填了一份快递单”地址、运送方式都写好了send是“把快递交到快递员手里”然后快递开始运输状态2、3直到快递员敲你的门把包裹送到状态4。前面几个状态你只能知道快递到哪了只有状态4你才真正拿到了包裹内容。注意很多初学者在状态为2或者3的时候就尝试读取responseText这时候数据要么还没到要么只到了一部分读出来的值自然不正确。所以处理数据的代码必须放在readyState 4的分支里。2.2 status状态码到底要怎么判断一个完整的HTTP响应除了响应体数据之外还包含一个状态行和响应头。状态行里的状态码是一个三位的数字比如200、404、500。XHR对象通过status属性暴露出这个数字。常见的处理逻辑是状态码是200或者一般说2xx范围才认为请求成功数据可用其他状态码统一按失败处理。这里有个场景容易踩坑请求成功与否取决于status和readyState两个条件同时满足——status是200到299之间readyState是4。很多初学代码只判断了readyState 4忽略了status结果在返回404错误页面的时候也去解析数据解析半天得到一个空字符串调试很久才发现原因。严谨的写法是xhr.onreadystatechange function () { if (xhr.readyState 4) { if (xhr.status 200 xhr.status 300) { // 这里的数据才是可信的 try { const data JSON.parse(xhr.responseText); console.log(data); } catch (e) { console.error(解析JSON失败, e); } } else { console.error(请求失败, xhr.status); } } };有人会问为什么不用等于200判断因为201已创建、204无内容、206部分内容也都是成功状态只判断等于200会把一些正常响应误判为失败。用区间判断是最稳妥的做法。2.3 常见的请求头设置和参数格式Ajax请求不光是把参数附加到URL上那么简单实际开发中经常需要设置请求头。最典型的场景是POST请求发送JSON数据时告诉服务器请求体是什么格式。如果不设置后台框架可能解析不到JSON参数接口直接报错。xhr.setRequestHeader(Content-Type, application/json;charsetutf-8);这行代码必须在open之后、send之前调用顺序错了会报错。另一个常见的请求头是Authorization用来传递Token做身份认证xhr.setRequestHeader(Authorization, Bearer token);GET和POST的参数传递方式有本质区别。GET参数拼在URL后面形如/api/user?id101name张三需要自己用encodeURIComponent处理特殊字符POST参数放在请求体里通过send方法传入。send里传什么呢取决于Content-Type——是表单格式就用URLSearchParams序列化是JSON格式就直接传JSON字符串。这两种格式恰好对应了后端框架里接收参数的两种方式搞混了就会遇到“前端明明传了参数后端说没收到”的经典问题。3. 实操过程与核心环节实现3.1 经典GET请求的完整写法先讲脚本化的原始写法因为不管后来封装的库多好用底层逻辑始终还是这一套。下面是一个按关键字搜索用户的GET请求示例function searchUser(keyword) { const xhr new XMLHttpRequest(); // 1. 设置请求行方法 地址 是否异步 xhr.open(GET, /api/user/search?kw encodeURIComponent(keyword), true); // 2. 注册状态监听 xhr.onreadystatechange function () { if (xhr.readyState 4) { if (xhr.status 200 xhr.status 300) { try { const result JSON.parse(xhr.responseText); renderUserList(result.data); } catch (err) { console.error(数据解析失败, err); } } else if (xhr.status 404) { alert(接口不存在请检查地址); } else { console.error(服务端异常, xhr.status); } } }; // 3. 发送请求 xhr.send(null); }注意这里的encodeURIComponent它是必须的。如果keyword里包含中文字符、空格、等特殊符号不编码的话URL可能在传输过程中被截断或者参数被错误解析。编码之后服务端框架一般会自动解码不需要你额外处理。补充一个小细节send(null)在GET请求里标准写法是传null相当于告诉浏览器没有请求体。有些老代码写成send()也问题不大。3.2 POST请求的两种不同格式POST请求最常踩的坑就是格式问题。举个例子登录接口通常需要传用户名和密码。如果我用form表单提交的格式正确的设置是const xhr new XMLHttpRequest(); xhr.open(POST, /api/login, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); const params new URLSearchParams(); params.append(username, username); params.append(password, password); xhr.send(params.toString());如果接口设计成接收JSON格式的请求体则要这样写const xhr new XMLHttpRequest(); xhr.open(POST, /api/login, true); xhr.setRequestHeader(Content-Type, application/json;charsetutf-8); const params { username: username, password: password }; xhr.send(JSON.stringify(params));两种格式在后端的接收方式不一样前者在多数框架里可以通过request.body.username或表单取值拿到后者普遍需要request.body配合JSON解析中间件才能拿到。实际开发中前后端约定好接口文档里的请求格式前端就按对应的方式组装数据。如果不确定打开浏览器开发者工具的Network面板看请求体长什么样——是usernameabcpassword123这样的明文字段串呢还是{username:abc}这样的JSON字符串一眼就能分辨。3.3 同步请求和异步请求的选择open方法的第三个参数是async默认是true表示异步。很多人不知道同步请求到底是怎么回事——把async设为false浏览器会等待请求返回之后才继续执行后面的JavaScript代码。看起来好像更简单但实际上这会造成严重的用户体验问题请求期间页面完全卡死鼠标点击没反应滚动也失灵。初学阶段偶尔用同步请求能避开回调嵌套的问题但正经做项目绝对不建议这么干。同步请求还可能导致浏览器地址栏都在等待复杂场景下甚至引发JS执行阻塞导致后续操作失效。正确的观念是Ajax存在的意义就是异步为了写法方便牺牲交互体验不值得。3.4 超时和网络异常处理请求发出去了到底有没有成功除了看status之外还要考虑请求一直没回来的情况。最典型的是网络断开、服务端过慢、DNS解析失败。XHR提供了timeout属性来设置超时时间单位是毫秒默认是0表示没有超时限制。开发中一般建议设置一个比如10秒xhr.timeout 10000; xhr.ontimeout function () { console.error(请求超时请检查网络后重试); };另外网络层面的异常比如断网、域名解析失败不会触发onreadystatechange到status为4的流程而是触发onerror事件。所以一个健壮的请求处理应该同时监听onerror和ontimeout。因为这类问题不像HTTP状态码那么直观有些错误是浏览器层面直接拒绝的连status都没有。4. 回调地狱与新的请求方案4.1 Ajax嵌套调用为什么让人头疼真实项目里很少只有一个请求。常见场景是先调登录接口拿用户信息再根据用户信息查订单列表然后根据订单ID查订单详情。如果用原始XHR一层层套下去代码会变成下面这样ajax(/api/login, function (user) { ajax(/api/orders?userId user.id, function (orders) { ajax(/api/orderDetail?orderId orders[0].id, function (detail) { // 渲染详情 }); }); });层级一旦超过三层代码就会变得难以读懂和维护这就是所谓的回调地狱。虽然功能上完全没有问题但任何人接手这个代码为了理清楚执行顺序都得花不少时间。后续要加一个超时处理、错误日志、重试逻辑都得在各个回调里分别修改极易漏改出错。4.2 Promise和fetch更现代的方式ES6引入Promise之后Ajax的痛苦有了很大的缓解。Promise可以把回调嵌套改成链式调用。即便不使用第三方库也可以用fetch这个现代API它就是基于Promise思想设计的fetch(/api/user/search?kw encodeURIComponent(keyword), { method: GET }) .then(function (response) { if (!response.ok) { throw new Error(请求失败状态码 response.status); } return response.json(); }) .then(function (data) { renderUserList(data); }) .catch(function (err) { console.error(请求出错, err); });fetch第一次阅读会有点陌生但仔细一看它的思路非常清晰第一步发起请求第二步判断响应状态第三步解析数据第四步处理错误。每一步都通过.then串联错误统一由.catch接手。和XHR对比不需要记忆readyState的四个状态不需要手动JSON.parse更不需要刻意设置onreadystatechange监听。4.3 async/await的完整形态Promise虽然已经比回调好很多但链式调用写多了还是不够直观。用async/await可以把异步代码写成几乎同步执行的样子async function searchUser(keyword) { try { const response await fetch(/api/user/search?kw encodeURIComponent(keyword)); if (!response.ok) { throw new Error(请求失败 response.status); } const result await response.json(); renderUserList(result); } catch (err) { console.error(请求出错, err); } }这段代码里await会“暂停”当前函数直到fetch返回的Promise完成然后继续往下走。如果在多数语言里这种写法叫做同步阻塞但在JavaScript里它不会阻塞主线程——await背后的机制还是会先把控制权交出去等结果完成后再恢复执行。所以用async/await写出来的代码虽然像同步代码页面性能和行为依然是异步的。注意async函数必定返回一个Promise调用方如果需要等待它结束应该用await调它或者用.then处理它返回的结果。很多人在这里容易忽略导致后续逻辑在请求完成之前就执行了。4.4 封装一个统一的请求函数实际项目中不会每次请求都现写一套而是会封装一个统一的函数。以fetch为例一个典型的封装需要考虑几件事基础路径、超时时间、请求头默认值、状态码统一处理、错误抛出。下面是一个简化的版本const request async (url, options {}) { const controller new AbortController(); const timer setTimeout(() controller.abort(), 10000); const config { method: options.method || GET, headers: { Content-Type: application/json, Authorization: Bearer (localStorage.getItem(token) || ), ...options.headers }, signal: controller.signal }; if (options.data) { config.body JSON.stringify(options.data); } try { const response await fetch(/api url, config); clearTimeout(timer); if (!response.ok) { if (response.status 401) { // 未登录或Token过期跳登录页 window.location.href /login; } const err new Error(请求失败 response.status); err.status response.status; throw err; } const contentType response.headers.get(Content-Type); if (contentType contentType.includes(application/json)) { return await response.json(); } return await response.text(); } catch (err) { if (err.name AbortError) { throw new Error(请求超时); } throw err; } }; // 使用方式 const userList await request(/user/search, { method: GET, headers: { X-Request-Id: getRequestId() } });这里用AbortController实现超时比XHR的timeout属性更灵活——不仅可以设置超时自动中断也可以供外部手动取消请求。这个封装解决了很多重复逻辑Token统一加、401统一跳登录、JSON自动解析、超时统一处理。实际项目中还可以继续扩展请求日志、重试机制、错误上报、竞态处理都可以在这个框架里逐步沉淀。5. 常见问题与排查技巧实录5.1 readyState停留在1或者0项目里偶尔会遇到状态始终卡在1的情况说明open已经执行了但send没有执行或者执行了但请求被某段逻辑阻止了。排查顺序一般是先确认send是否被调用再确认URL地址是否正确、协议有没有被Mixed Content策略拦截例如在HTTPS页面请求HTTP地址最后看代码是否提前return或者抛异常了。控制台如果报错Failed to load resource: net::ERR_MIXED_CONTENT基本都是这个原因。5.2 后端返回了数据但前端解析不到这种现象很经典Network面板里能看到响应但代码里JSON.parse报错或者拿到的responseText是空的。大概率是内容类型不对。比如服务器返回的Content-Type是text/plain但实际内容又是一段JSON这种情况建议先打印responseText确认响应内容本身而不是直接parse。另一个常见情况是响应里带有BOM头或者是多层JSON嵌套导致的字符串转义问题。我的排查习惯是先console.log(xhr.responseText)看原始内容确认内容格式之后再针对parse。5.3 GET请求中文参数乱码中文参数如果在Network面板里显示成%E5%BC%A0%E4%B8%89这种编码串通常是没问题的服务端会自动解。真正乱码的情况多数是前端没有encodeURIComponent导致浏览器替你编码但特殊字符被错误处理或者后端接收时用了错误的解码字符集。规范的做法是前端显式encodeURIComponent后端统一按UTF-8解析两边都统一乱码问题就能避免。5.4 页面跳转和Ajax竞态一个高频坑用户点按钮触发Ajax请求在请求还没返回时又切走了页面结果响应回来后在页面上的一个已经不存在的DOM节点上操作控制台报错。解决思路有两种一是请求发起后记录一个“取消标记”在节点操作前判断页面是否还存在二是用AbortController在页面隐藏或组件销毁时主动取消请求。这在前端框架里对应的是“组件卸载后不能更新状态”的警告底层逻辑是一致的。5.5 网络面板是最好的排障工具调试Ajax问题浏览器自带的Network面板是首选工具远比打日志高效。具体做法是F12打开开发者工具切到Network标签页勾选Fetch/XHR筛选请求重新触发页面上的请求动作面板里会出现这次请求的记录。点击这条记录能看到完整的请求地址、请求方法、请求头、请求体、响应状态码、响应体和耗时。前后端联调时一遍看Response一遍看Payload问题出现在哪一侧基本都能快速定位。再配合Console面板看console.error输出大多数问题都能在几分钟内查清楚。6. 一点个人心得我从原始XHR一路写到现在最大的体会是Ajax这套东西技术栈换了好几轮但核心依然没变——请求、响应、状态、异常。不管用哪种方案思路都是一样的。还在用原始XHR的读者我建议试试fetch它的表达方式更符合现代JS的直觉但也不要扔掉XHR的知识某些老项目维护或者需要上传进度条时候XHR自带的上传进度事件对应progress事件依然比fetch方便得多。另外分享一个很多人在正式项目里用到的习惯给所有请求统一加上“请求ID”每次发请求时生成一个唯一标识放进请求头。后端可以把请求ID打印在日志里前端也把请求ID放进console的输出里。这样前后端对同一个请求的排查只要报出请求ID两边能严丝合缝地对应上联调效率会高非常多。最后记住一条凡是涉及网络通信的逻辑都需要同时处理成功和失败两条路径失败路径里有状态码异常、网络异常、超时异常、数据格式异常。把一个异常处理写全了后面维护的时候能替你挡掉很多莫名其妙的问题。
延伸阅读

更多相关文章

2026/10/20 5:36:24

GESP C++二级选择题考点拆解:避坑指南与得分技巧

先说结论:如果你正在准备GESP C二级认证,2024年3月这套卷子的第一部分选择题前8题,整体难度不算高,但很多人恰恰栽在“简单题”上。我认识不少学生,后面大题能写出来,反而选择题错一半。原因不是知识点不会…

2026/10/20 5:36:24

双卡A6000先验哪条路径?LingBot微调与RAG的验证策略

说实话,看到这个标题的第一反应是:团队刚拿到两台 A6000,就急着给 LingBot 定路线,这本身就值得先刹车。硬件到位、项目代号也有了,但“先验哪条路径”这个问题,十有八九不是在问技术,而是在问“…

2026/10/20 5:36:24

Kimi、豆包、ChatGPT生成的文本怎么降AI率?对症下药是关键

这段时间前后碰到好几个同学过来问同一件事:用AI写的论文,到了查重和检测环节被平台标记出来,问我该怎么处理。他们用的工具还不一样,有的拿Kimi写的初稿,有的拿豆包写的,有的直接让ChatGPT给整篇出文。试了…

2026/10/20 7:31:33

基于Matlab的多智能体一致性算法仿真:从原理到代码实现

做机器人编队、分布式控制或者群体系统仿真的人,对“多智能体”和“一致性算法”这两个词肯定不陌生。这是多智能体系统里最基础、也最常用的一套协议:每个机器人只跟有限几个邻居交换位置信息,每个个体都按同一个局部规则更新自己的运动&…

2026/10/20 7:31:33

Testcontainers Python 等待策略全解析:告别容器测试随机超时

很多人第一次接触 Testcontainers Python 时,第一个感觉是“挺方便”,第二个感觉往往是“怎么这么不稳”。容器明明start()成功了,可代码一连接就超时;本地跑得好好的测试,换到 CI 环境就开始随机失败。折腾到最后&…

2026/10/20 7:31:33

AI-native芯片设计:需求工程、跨层优化与验证思维才是真正稀缺能力

最近某头部AI实验室放出的那份AI-native芯片设计研究报告,业内讨论热度比我预期的要高得多。有意思的是,我翻了一圈各路评论,发现大家争论的焦点基本集中在模型参数、数据规模、自动化覆盖率这些技术指标上,好像只要把这些数字做上…

2026/10/20 7:31:33

Spring Boot宠物用品交易平台实战:从下单链路到并发库存扣减

最近在整理手头的项目资源时,翻到一套比较完整的Java Spring Boot宠物用品交易平台项目,带源码、开发文档、运行视频和讲解视频。这类垂直电商项目在练手阶段确实值得花时间抠一遍,因为它不像网上那些"增删改查管理系统"一样流于形…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑