资讯动态

AJAX从入门到实践:手写XHR、封装Promise与Fetch对比

发布时间:2026/10/17 13:57:12 来源:尧图企业网站定制
你有没有遇到过这种情况页面上的搜索框还没输完关键词整个页面就白屏了点一下“加载更多”结果整张网页刷新了一遍刚才滚动的位置全没了。这种体验放在今天几乎让人无法忍受。而解决这个问题的关键就是AJAX。AJAXAsynchronous JavaScript and XML直译过来是“异步的JavaScript和XML”但它实际的含义要比这个名字宽泛得多。它代表的是一整套在不刷新页面的前提下与服务器交换数据并局部更新页面的技术方案。说通俗点AJAX让网页像桌面软件一样可以在后台悄悄完成数据交互。这篇内容会从零开始拆解AJAX的来龙去脉、底层对象、手写请求的每一个步骤、封装思路、常见坑点以及它和现代Fetch API的关系。写给你这样的前端初学者、刚接触前后端联调的后端同学以及所有想彻底弄懂浏览器网络请求原理的人。1. 先搞清楚AJAX到底解决什么问题学习一项技术之前先明白它解决什么痛点比急着写代码重要得多。AJAX的出现针对的是传统Web应用里“页面刷新”这个老大难问题。1.1 没有AJAX的时代有多痛苦在AJAX大规模应用之前Web页面是典型的“请求-响应-刷新”模式。你提交一个表单浏览器把数据打包发给服务器服务器处理完返回一个全新的HTML页面浏览器丢弃当前页面重新渲染返回的内容。如果只是切换一个Tab标签、点赞一条动态、修改一个用户名也得整页刷新。这里面的体验问题很明显。第一状态丢失。用户填了一半的表单、滚动到的位置、页面里临时修改过的样式全部重置。第二带宽浪费。很多时候我们只需要更新页面里5%的内容却要把100%的HTML重新传一遍。第三等待感强。整页刷新在流畅度上天生有上限因为浏览器要经历“销毁旧页面→加载新页面→解析渲染”的全过程。我早期做一个后台管理项目时体会特别深。表格里有个“启用/禁用”状态的开关每切换一次就要整页刷新页面顶部还会闪一下白色的过渡背景产品经理看了直接摇头。后来改成AJAX请求只更新那一行的状态整个操作在用户眼里就是“秒级完成”那种体验提升是质变的。1.2 局部更新AJAX的核心思路AJAX的本质思路可以概括成四个字局部更新。它做的事情其实是把“浏览器整体跳转”拆成了两个独立动作。第一个动作是页面里的JavaScript在后台创建请求发给服务器第二个动作是服务器只返回需要的数据通常是JSON或者XML片段而不是完整页面拿到数据后JavaScript再主动改动DOM更新页面上需要变的那一小块内容。这里有个关键认知需要强调AJAX没有发明任何新的通信协议它用的是HTTP协议本身。它真正的价值在于把“谁来发起请求、谁来解析响应”这件事从浏览器内核转移到了JavaScript手里。你可以把它理解为以前看门大爷浏览器只负责整栋楼的人员进出登记AJAX出现后你JavaScript能够单独开一个小窗口只接收自己需要的包裹不用惊动整栋楼的人。1.3 底层拼图XHR对象与浏览器事件循环聊到AJAX的底层实现绕不开一个核心对象XMLHttpRequest简称XHR。早期AJAX几乎就是XHR的同义词。这个对象由浏览器提供专门用于脚本发起HTTP请求。虽然名字里带着XML但它实际上可以接收任何文本类型的数据JSON、纯文本、甚至HTML片段都没问题。还有一个量子层面的东西需要理解就是异步机制。XHR提供了异步模式意味着当你调用send方法之后JavaScript不会傻等服务器响应而是继续往下执行其他代码。那么“响应回来了之后做什么”怎么控制答案是回调函数。XHR对象内部维护着状态变化机制每当状态改变时触发事件你在事件回调里处理响应数据。从浏览器事件循环的角度看XHR的异步回调并不是立刻执行的而是等当前执行栈清空之后由事件循环从任务队列里取出来执行。这解释了为什么你在send之后马上打印响应数据总是拿到空值因为回调根本没跑。这个原理理解透了后面排查“为什么数据总是拿不到”之类的问题思路会清晰很多。2. 原生AJAX五步法从零手写一个请求纸上谈兵没意思咱们直接上手写代码。原生XHR的完整流程可以拆成五个步骤看完你就会发现这个传说中很复杂的东西其实就那么回事。2.1 五步法完整代码与逐行解析以下是一段最基础的原生AJAX请求代码我习惯把每一个环节都注释清楚// 第一步创建XMLHttpRequest对象 var xhr new XMLHttpRequest(); // 第二步配置请求open方法接收三个参数 // 参数1请求方式GET/POST等 // 参数2请求地址 // 参数3是否异步true表示异步false表示同步强烈不建议同步 xhr.open(GET, https://api.example.com/users?id1, true); // 第三步绑定状态变化监听 xhr.onreadystatechange function () { // readyState为4表示请求已完成responseText里已经能拿到响应数据 if (xhr.readyState 4) { // 判断HTTP状态码200到300之间表示正常 if (xhr.status 200 xhr.status 300) { // 第四步处理响应数据这里以JSON字符串为例 var data JSON.parse(xhr.responseText); console.log(data); // 第五步在这里更新DOM也就是局部刷新 document.getElementById(username).textContent data.username; } else { console.error(请求失败状态码 xhr.status); } } }; // 第四步实际上顺序上在事件绑定之后发起请求 xhr.send();这段代码跑通之后AJAX的整体脉络就清晰了创建对象、配置请求、挂监听、发送请求、在回调里处理数据。2.2 readyState的五个阶段必须背下来的状态码认识XHR之后你首先要熟悉的是它的readyState属性。这个属性有5个值对应请求从创建到完成的5个阶段readyState含义说明0未初始化XHR对象已创建open方法还未调用1已打开open方法已调用send方法还未调用2已发送send方法已调用请求头已发送响应头尚未接收3接收中正在接收响应体部分数据已到达4已完成响应体接收完毕可以访问responseText真正需要关注的只有4这个状态。很多初学者容易混淆readyState和HTTP状态码。readyState描述的是XHR对象自身的生命周期阶段status才是服务器返回的HTTP状态码比如200是成功、404是资源不存在、500是服务器内部错误。判断一个请求是否成功逻辑上要先看readyState是否为4再看status是否为2xx。2.3 一段容易忽略的细节同步与异步的选择我特意提一下open方法的第三个参数因为见过不少人在这个参数上踩坑。如果传false请求会变成同步模式JavaScript会死等服务器响应期间页面脚本完全卡死用户体验极差浏览器也早就把同步XHR标记为废弃特性了。更隐蔽的问题是同步模式下onreadystatechange回调里的代码只能在send执行之后由浏览器自行触发而由于没有了异步事件循环的参与有些边界情况会产生非常费解的行为。我的建议简单粗暴永远用异步模式永远不要在生产环境写同步XHR。3. 请求方式详解GET与POST的正确打开方式有了上面的基础接下来该处理真实开发中绕不开的细节了。GET、POST是最常用的两个请求方式看似简单但传参方式、编码处理、Content-Type设置上有很多讲究坑还是一个接一个。3.1 GET请求传参URL查询字符串的拼装与编码GET请求的特点是把参数拼在URL的查询字符串里形如sitename?page2size10。手动拼字符串容易忽略编码问题。如果参数值里有中文、空格、特殊符号比如name张三city北京直接拼上去大概率会出现乱码或者请求失败。正确的做法是用encodeURIComponent对参数值做编码。我习惯写一个小工具函数来拼参数function buildQueryString(params) { return Object.keys(params) .map(function (key) { return encodeURIComponent(key) encodeURIComponent(params[key]); }) .join(); } var query buildQueryString({ keyword: 前端开发, page: 1, size: 10 }); var xhr new XMLHttpRequest(); xhr.open(GET, https://api.example.com/search? query, true); xhr.send();不要小看这一步我见过很多人排查半天乱码问题最后发现就是忘了编码。3.2 POST请求传参Content-Type决定服务器怎么解析POST请求的参数放在请求体里但服务器侧不是天然知道你的请求体是什么格式。它靠请求头里的Content-Type字段来判断。两种最常见的Content-Type对应两种编码方式Content-Type格式典型场景application/x-www-form-urlencodedkeyvaluekey2value2传统表单提交application/jsonJSON字符串前后端分离接口如果你要模拟传统表单提交需要在send之前设置请求头var xhr new XMLHttpRequest(); xhr.open(POST, https://api.example.com/login, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded); xhr.send(usernameadminpassword123456);如果你要和现代后端接口联调发送JSON数据是主流var xhr new XMLHttpRequest(); xhr.open(POST, https://api.example.com/users, true); xhr.setRequestHeader(Content-Type, application/json); var payload JSON.stringify({ name: 张三, age: 25 }); xhr.send(payload);这里有三个高频报错场景。第一个发送JSON时忘了设置Content-Type为application/json后端把JSON字符串当成了表单参数解析拿不到值。第二个已经设置了application/json但是send里面传的是普通字符串没有JSON.stringify后端收到的是格式错误的JSON,直接报解析异常。第三个设置了Content-Type但服务器不支持跨域请求头的预检报了CORS错误。后面两个问题在排查章节里细说。3.3 响应处理与JSON解析的边界陷阱拿到响应之后responseText是字符串。如果接口约定返回JSON你需要自己调用JSON.parse解析。问题在于实际接口返回的数据可能是空的、可能是纯文本、可能是一个错误页面的HTML盲目JSON.parse会直接抛异常。我的习惯是先做简单的类型判断和容错xhr.onreadystatechange function () { if (xhr.readyState ! 4) return; if (xhr.status 200 || xhr.status 300) { console.error(接口错误, xhr.status, xhr.responseText); return; } var result; try { result JSON.parse(xhr.responseText); } catch (e) { // 如果解析失败直接按文本处理 result xhr.responseText; } // 继续处理result console.log(result); };加上try-catch之后调试成本会低很多不会因为一行解析就导致整个回调崩溃。4. 封装一个通用请求函数终结重复劳动日常开发中你不可能每次都写五步原生代码。把重复的东西封装成工具函数是提升效率的必经之路。这里我推荐用Promise把XHR包起来彻底告别回调地狱。4.1 从回调嵌套到Promise封装用Promise包装XHR的好处是可以链式调用then处理成功情况用catch统一捕获错误还可以配合async/await写出同步风格的代码。以下是我在实际项目中用过的封装版本代码不长但足够实用function ajax(options) { var method options.method || GET; var url options.url; var data options.data || null; var headers options.headers || {}; return new Promise(function (resolve, reject) { var xhr new XMLHttpRequest(); // GET类请求把data拼到URL上其他请求放到send里 if (method GET data) { var query buildQueryString(data); url (url.indexOf(?) 0 ? : ?) query; } xhr.open(method, url, true); // 设置请求头 Object.keys(headers).forEach(function (key) { xhr.setRequestHeader(key, headers[key]); }); xhr.onreadystatechange function () { if (xhr.readyState ! 4) return; if (xhr.status 200 xhr.status 300) { var result; try { result JSON.parse(xhr.responseText); } catch (e) { result xhr.responseText; } resolve(result); } else { reject(new Error(请求失败状态码 xhr.status 信息 xhr.responseText)); } }; xhr.onerror function () { reject(new Error(网络异常或跨域错误)); }; if (method GET) { xhr.send(); } else { // 如果data是对象默认转成JSON字符串 xhr.send(typeof data string ? data : JSON.stringify(data)); } }); }调用起来就非常清爽了ajax({ method: GET, url: https://api.example.com/users?id1 }) .then(function (data) { console.log(拿到用户, data); }) .catch(function (err) { console.error(err); }); // 配合async/await async function loadUser() { try { var user await ajax({ method: POST, url: https://api.example.com/login, headers: { Content-Type: application/json }, data: { username: admin, password: 123456 } }); console.log(user); } catch (e) { console.error(登录失败, e); } }4.2 封装里容易忽略的几个设计细节封装不是写完能跑就完事。有两个设计点值得展开说。第一个是超时处理。真实网络环境不会永远顺畅请求可能因为网络抖动长时间不返回。XHR原生没有好用的超时回调我的做法是用setTimeout外加手动abortfunction ajaxWithTimeout(options, timeoutMs) { return new Promise(function (resolve, reject) { var xhr new XMLHttpRequest(); // ...配置和监听 var timer setTimeout(function () { xhr.abort(); reject(new Error(请求超时)); }, timeoutMs || 10000); xhr.onreadystatechange function () { if (xhr.readyState 4) { clearTimeout(timer); // ...后续处理 } }; // ...发送请求 }); }第二个是对error和abort事件的处理。onerror不仅在网络断开时触发跨域失败也会触发这会误导排查方向。我在实战里遇到的情况是后端接口地址配错导致404结果onerror没触发onreadystatechange里status是404这还算好排查。但如果是协议写错、域名拼错导致连接失败onerror就会触发如果没有仔细看控制台很容易误判成跨域问题。4.3 关于axios的一个理解视角现在很多同学一上来就用axios这没问题。但我想说一个很容易被忽略的事实axios在浏览器环境里底层就是一个封装了XHR的Promise库。你在axios里配置url、method、data、headers这些选项最终都是映射到XHR的对应属性和方法上。所以掌握原生XHR本质上就是掌握了axios的核心原理。理解了XHR的缓冲机制你才能明白为什么axios要分interceptor也才能看懂axios源码里那些复杂的配置合并逻辑。我的建议是至少完整手写一遍上面的封装再回过头用axios你会觉得一切顺理成章。5. 现代替代者Fetch API的优势与它的坑如果你刚开始学习AJAX很可能困惑既然有了fetch为什么还要学XHR答案是fetch确实是更现代、更优雅的方案但XHR依然有fetch无法完全替代的角落。5.1 Fetch基本用法三行代码搞定请求Fetch API是浏览器原生提供的接口基于Promise设计语法非常简洁// 最简单的GET请求 fetch(https://api.example.com/users?id1) .then(function (response) { // 注意这里拿到的不是数据本身而是一个Response对象 return response.json(); }) .then(function (data) { console.log(data); }); // POST请求 fetch(https://api.example.com/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: 张三 }) }) .then(function (response) { return response.json(); }) .then(function (data) { console.log(data); });配合async/await观感完全媲美后端代码async function submitUser() { var response await fetch(https://api.example.com/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: 李四 }) }); var data await response.json(); console.log(data); }5.2 Fetch与XHR的对比各有什么取舍我把两者的核心差异整理成一张表读起来更直观对比项XMLHttpRequestFetch基础机制事件回调Promise代码简洁度繁琐需要多步配置简洁链式调用请求中止支持abort方法默认不支持需借助AbortController上传进度支持监听progress事件不支持原生上传进度只能自己实现默认行为不发送cookies之外的凭据默认不会携带cookies需要配置credentials错误处理网络错误触发onerrorHTTP错误需手动判断只有网络错误会rejectHTTP 404/500也会resolve需手动处理fetch的一大优点是默认不会携带跨域cookie这在某些场景下是安全特性但也常常导致“登录失效”的困惑。XHR默认在同源请求下携带cookie行为更直观。5.3 Fetch最容易被忽略的两个细节第一个是响应对象需要显式解析。fetch返回的是Response对象里面的body是一个可读流。你必须先调用response.json()、response.text()等方法把流读完才能拿到数据。如果你忘记调用或者调用了两次第二次会报错因为流已经消费完了。这和XHR的responseText直接拿来就用的设计完全不同。第二个是HTTP错误状态不会reject。这个坑坑了无数人。你用fetch请求一个404地址它不会走进catch而是正常走进then只是response.ok为false、status为404。这意味着你必须手动检查var response await fetch(https://api.example.com/nonexistent); if (!response.ok) { throw new Error(HTTP错误 response.status); } var data await response.json();如果没做这个判断接口返回404时你的页面会带着一段错误数据继续运行排查起来非常诡异。6. 实战把一个小留言板做成无刷新交互理论讲再多不如一个完整Demo有说服力。下面手把手做一个极简留言板前端页面用原生XHR发请求后端我用JSON Server模拟接口整体走通“加载列表→提交留言→局部刷新”的完整链路。6.1 环境准备快速启动一个本地模拟接口不需要真的写后端用JSON Server这个工具几秒钟就能搭出REST接口。先初始化项目并安装npm init -y npm install json-server然后在项目根目录创建db.json{ messages: [ { id: 1, content: 第一条留言, createdAt: 2024-01-01 10:00:00 }, { id: 2, content: 第二条留言, createdAt: 2024-01-02 12:30:00 } ] }启动服务npx json-server --watch db.json --port 3000这时本地就有了一个完整的REST接口GET http://localhost:3000/messages 返回留言列表POST http://localhost:3000/messages 新增留言。JSON Server默认支持CORS用浏览器直接请求也不会被跨域挡住。6.2 前端页面实现加载与提交两个核心动作新建一个index.html核心代码如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleAJAX留言板/title /head body h1无刷新留言板/h1 ul idmessageList/ul input typetext idmessageInput placeholder输入留言内容 button idsubmitBtn提交/button script // 封装好的ajax函数直接复用上一节的内容 function ajax(options) { // ...省略与前面封装代码一致 } // 加载留言列表 function loadMessages() { ajax({ method: GET, url: http://localhost:3000/messages }) .then(function (list) { var ul document.getElementById(messageList); ul.innerHTML ; list.forEach(function (item) { var li document.createElement(li); li.textContent item.content item.createdAt ; ul.appendChild(li); }); }) .catch(function (err) { console.error(加载失败, err); alert(留言加载失败请检查后端服务是否启动); }); } // 提交新留言 function submitMessage() { var input document.getElementById(messageInput); var content input.value.trim(); if (!content) { alert(留言内容不能为空); return; } ajax({ method: POST, url: http://localhost:3000/messages, headers: { Content-Type: application/json }, data: { content: content, createdAt: new Date().toLocaleString() } }) .then(function () { input.value ; // 关键点提交成功不需要刷新页面直接重新拉一次列表 loadMessages(); }) .catch(function (err) { console.error(提交失败, err); alert(提交失败); }); } // 绑定事件 document.getElementById(submitBtn).addEventListener(click, submitMessage); // 页面加载时初始化 loadMessages(); /script /body /html打开浏览器访问index.html你会看到一条体验主线点击提交按钮页面不发生任何跳转留言列表自动更新新内容出现在最后。整个过程没有任何一次整页刷新。这就是AJAX局部更新的直观体现。6.3 进阶加上删除操作体会数据交互全流程留言板只有加载和提交还不够完整。再加上一个删除功能你就能完整感受AJAX的“增删改查”全流程了。在每条留言后面加一个删除按钮点击后发送DELETE请求function deleteMessage(id) { ajax({ method: DELETE, url: http://localhost:3000/messages/ id }) .then(function () { loadMessages(); }) .catch(function (err) { console.error(删除失败, err); }); }注意这里的DELETE请求没有请求体XHR的send方法直接留空。JSON Server会自动根据id删除对应数据。删除成功后同样调用loadMessages()因为重新拉取列表的方式在数据量小的时候最稳妥不需要手动操作DOM去移除一条li。7. 高频问题排查与避坑指南最后分享一些实战中最高频的问题和排查技巧。这些内容大多来自我自己的踩坑记录你早晚会用到。7.1 如何快速定位请求有没有发出去遇到“页面没反应”第一件事不是看代码逻辑而是打开浏览器的开发者工具切到Network网络面板按F5刷新页面观察请求列表。如果请求压根没出现在列表里说明代码没执行到send那一步问题在前端逻辑。如果请求出现在列表里但状态是红色说明请求发出去了但服务器没按预期返回问题在后端接口或者参数。如果请求状态正常但页面没更新问题在响应数据处理或者DOM更新逻辑。这个排查顺序能帮你至少省下半小时的无头苍蝇式查找。7.2 跨域问题前端最经典的拦路虎跨域是AJAX绕不开的坎。浏览器的同源策略会阻止跨域请求读取响应。你在本地开发时请求一个不同端口的接口十有八九会报“CORS error”。要解决开发环境的跨域常见方案有三种。第一种后端在响应头加上Access-Control-Allow-Origin允许指定域名跨域。第二种开发环境配置代理让请求先发到同源地址再由代理转发到目标接口。第三种用JSON Server这类工具自带的CORS支持来快速开发测试。判断是不是跨域问题的标志也很明显控制台报了“blocked by CORS policy”相关错误请求在Network面板里是“失败”或者“被阻止”但后端日志里明明收到了请求。这时候别去纠结前端代码直接找后端或者配代理。7.3 缓存问题GET请求的“旧数据”陷阱GET请求在有些浏览器场景下会被本地缓存。比如你连续请求同一个URL两次第二次可能直接命中缓存拿到的还是上一次的数据。这在轮询最新状态时特别容易造成困扰。解决方案有两种常用手段。第一种是在URL后加随机参数让每次请求的URL都不同var url https://api.example.com/current?timestamp Date.now();第二种是在响应头里设置禁止缓存。对于前端开发者来说第一种方式更简单可控开发阶段随手加一个时间戳就能避免缓存干扰。7.4 乱码与编码问题最容易被忽略的细节AJAX请求返回的数据出现乱码常见原因有两个。第一个是服务器返回的字符集和前端解析字符集不一致。第二个是GET请求里的参数从URL读取时和页面编码不一致。排查乱码问题时先看Network面板里的响应头Content-Type有没有指定charset再看页面meta声明的是什么编码。保持页面、接口、数据库三者编码一致能避免绝大多数乱码问题。7.5 调试经验把请求信息打印在控制台很多人调试XHR时只会在回调里console.log数据数据没回来就一脸茫然。我的习惯是在request的各个阶段都打印请求信息xhr.onreadystatechange function () { console.log(当前readyState, xhr.readyState, HTTP状态, xhr.status); if (xhr.readyState 4) { console.log(响应文本, xhr.responseText); } };这样一来请求到底卡在哪一步是没发出去、网络错误、还是后端返回异常直观可见。开发完成后记得删掉这些调试日志就好。聊到这里AJAX的核心内容基本已经覆盖完整了。以我个人经验来看学习AJAX最好的路径就是先手写一遍原生XHR的完整流程再用Promise封装一个自己的ajax工具函数最后去理解fetch和axios为什么那样设计。这个过程走下来你不仅学会了AJAX还会对整个浏览器网络请求模型建立直观看得见、摸得着的感知。很多年前我在学习这个知识点时也曾觉得XHR的API繁琐又难记但当我真正手写完整流程并封装成工具函数之后再去看各种框架的网络层都像看老朋友一样熟悉。踩过的坑再多也值得因为AJAX是前端与后端对话的基石地基打牢了后面盖多高的楼都不慌。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑