在现代 Web 开发中,“异步” 是贯穿始终的核心场景 —— 加载数据、提交表单、文件上传,几乎所有与服务器交互的操作都依赖异步机制。而 Ajax、Promise、fetch 作为异步通信的三大核心工具,再加上支撑它们运行的 JS 内存模型,共同构成了前端异步编程的基石。
本文将从 “基础用法→核心原理→底层逻辑” 层层递进,帮你彻底搞懂这四个概念的关联与实践,解决实际开发中 “异步执行混乱”“请求封装踩坑”“内存模型抽象难懂” 等问题。
一、Ajax:异步通信的 “开山鼻祖”
在 Ajax 出现之前,Web 应用是 “同步刷新” 的 —— 每次请求服务器都要刷新整个页面,用户体验极差。而 Ajax(Asynchronous JavaScript and XML)的诞生,让 “局部刷新” 成为可能,彻底改变了 Web 应用的交互模式。
1. 什么是 Ajax?
Ajax 不是一门新技术,而是一种异步通信方案:通过 JavaScript 异步发送 HTTP 请求,获取服务器数据后,局部更新页面 DOM,无需刷新整个页面。
核心作用:在不中断用户操作的前提下,与服务器后台通信。
2. 核心原理:XMLHttpRequest(XHR)
早期的 Ajax 依赖浏览器原生的XMLHttpRequest对象(简称 XHR),这是实现异步请求的核心 API。
原生 Ajax 基础示例(GET 请求)
javascript
运行
// 1. 创建XHR对象
const xhr = new XMLHttpRequest();
// 2. 配置请求(请求方式、URL、是否异步)
xhr.open('GET', 'https://api.example.com/data', true);
// 3. 监听响应状态变化
xhr.onreadystatechange = function() {
// readyState=4:请求完成;status=200:响应成功
if (xhr.readyState === 4 && xhr.status === 200) {
// 解析响应数据(早期用XML,现在主流JSON)
const data = JSON.parse(xhr.responseText);
console.log('请求成功:', data);
// 局部更新DOM
document.getElementById('content').innerText = data.msg;
}
};
// 4. 发送请求
xhr.send();
关键说明:
readyState:请求状态(0 = 未初始化,1 = 已打开,2 = 已发送,3 = 正在接收响应,4 = 请求完成)。status:HTTP 状态码(200 = 成功,404 = 资源不存在,500 = 服务器错误等)。- 异步特性:
open的第三个参数为true时,请求不会阻塞后续代码执行。
3. 原生 Ajax 的痛点
虽然 XHR 实现了异步通信,但在实际开发中暴露了明显问题:
- 回调地狱:多个串行异步请求时,嵌套层级极深(比如请求 A 成功后请求 B,B 成功后请求 C)。
- API 繁琐:配置请求、监听状态、解析数据步骤分散,代码冗余。
- 错误处理不统一:需要手动判断
readyState和status,异常捕获复杂。
这些痛点,催生了 Promise 的出现。
二、Promise:异步流程的 “指挥官”
Promise 是 ES6 引入的异步流程控制工具,核心目标是解决 “回调地狱”,让异步逻辑变得线性、可维护。
1. 为什么需要 Promise?
先看一段 “回调地狱” 的代码(用原生 Ajax 串行请求):
javascript
运行
// 回调地狱示例:请求A → 请求B → 请求C
xhr1.open('GET', '/api/a', true);
xhr1.onreadystatechange = function() {
if (xhr1.readyState === 4 && xhr1.status === 200) {
const dataA = JSON.parse(xhr1.responseText);
// 请求B依赖A的结果
xhr2.open('GET', `/api/b?aId=${dataA.id}`, true);
xhr2.onreadystatechange = function() {
if (xhr2.readyState === 4 && xhr2.status === 200) {
const dataB = JSON.parse(xhr2.responseText);
// 请求C依赖B的结果
xhr3.open('GET', `/api/c?bId=${dataB.id}`, true);
xhr3.onreadystatechange = function() {
// ... 无限嵌套
};
xhr3.send();
}
};
xhr2.send();
}
};
xhr1.send();
嵌套层级越多,代码越难维护 —— 这就是 Promise 要解决的核心问题。
2. Promise 核心特性
- 三种状态:
pending(等待中)→fulfilled(成功)/rejected(失败),状态一旦改变不可逆。 - 链式调用:通过
.then()处理成功结果,.catch()捕获错误,支持链式串联多个异步任务。 - 异步分离:将 “异步任务执行” 和 “结果处理” 分离,代码结构更清晰。
3. 实战:用 Promise 封装 Ajax
将原生 XHR 封装成 Promise,彻底解决回调地狱:
javascript
运行
// 封装Promise版Ajax
function request(url, method = 'GET', data = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 处理GET请求参数(拼接URL)
if (method === 'GET' && Object.keys(data).length) {
const params = new URLSearchParams(data).toString();
url = `${url}?${params}`;
}
xhr.open(method, url, true);
// 设置请求头(POST请求需指定Content-Type)
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功:解析JSON并传递给then
resolve(JSON.parse(xhr.responseText));
} else {
// 失败:传递错误信息给catch
reject(new Error(`请求失败:${xhr.status}`));
}
}
};
// 处理POST请求数据(转为JSON字符串)
const sendData = method === 'POST' ? JSON.stringify(data) : null;
xhr.send(sendData);
});
}
// 链式调用:解决回调地狱
request('/api/a')
.then(dataA => {
// 请求A成功后,请求B
return request('/api/b', 'GET', { aId: dataA.id });
})
.then(dataB => {
// 请求B成功后,请求C
return request('/api/c', 'GET', { bId: dataB.id });
})
.then(dataC => {
console.log('最终结果:', dataC);
})
.catch(error => {
// 统一捕获所有请求错误
console.error('请求异常:', error);
});
核心改进:
- 串行异步任务通过
.then()链式串联,代码线性排列,可读性大幅提升。 - 所有错误通过
.catch()统一捕获,无需重复判断状态码。
三、fetch:现代异步通信的 “新宠”
fetch 是 ES2015 + 推出的新一代异步请求 API,基于 Promise 设计,旨在替代传统 XHR。它的 API 更简洁、语义更清晰,是目前前端异步通信的主流方案。
1. fetch 核心特点
- 基于 Promise:天然支持链式调用,无需手动封装。
- API 简洁:
fetch(url, options)一行代码发起请求。 - 支持现代特性:默认支持 Promise、可搭配
async/await、支持 Request/Response 对象。
2. 基本用法(GET/POST 请求)
javascript
运行
// 1. GET请求(默认方法)
fetch('https://api.example.com/data')
.then(response => {
// 第一步:判断响应状态(fetch的坑:404/500不会reject,需手动处理)
if (!response.ok) {
throw new Error(`HTTP错误:${response.status}`);
}
// 第二步:解析响应数据(支持json()/text()/blob()等)
return response.json();
})
.then(data => {
console.log('GET请求成功:', data);
})
.catch(error => {
console.error('错误:', error);
});
// 2. POST请求(带请求体和请求头)
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: '张三', password: '123' }), // 请求体需转为字符串
})
.then(response => response.json())
.then(data => console.log('POST请求成功:', data))
.catch(error => console.error('错误:', error));
3. fetch 避坑指南(关键!)
fetch 虽好,但有几个容易踩的坑,必须注意:
- 坑 1:404/500 不触发 reject:只有网络错误(如断网)才会 reject,HTTP 错误状态码(4xx/5xx)仍会 resolve,需通过
response.ok(状态码 200-299 为 true)手动判断。 - 坑 2:默认不发送 Cookie:跨域请求或需要身份验证时,需添加
credentials: 'include'选项。 - 坑 3:响应需手动解析:
fetch返回的是Response对象,不是直接的数据,需通过response.json()/response.text()等方法解析(返回 Promise)。 - 坑 4:不支持超时设置:需手动用
Promise.race()实现超时控制。
解决超时问题示例:
javascript
运行
// 封装带超时的fetch
function fetchWithTimeout(url, options = {}, timeout = 5000) {
// 超时Promise:超过时间后reject
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), timeout);
});
// 竞速:谁先完成就用谁的结果
return Promise.race([
fetch(url, options),
timeoutPromise
]);
}
// 使用
fetchWithTimeout('/api/data', {}, 3000)
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error(error)); // 超时会进入这里
4. Ajax vs fetch 核心对比
| 特性 | Ajax(XHR) | fetch |
|---|---|---|
| 底层 API | XMLHttpRequest 对象 | 浏览器原生 fetch API |
| Promise 支持 | 需手动封装 | 天然支持 |
| API 简洁度 | 繁琐(多步骤配置) | 简洁(一行代码发起请求) |
| 错误处理 | 需判断 readyState 和 status | 网络错误 reject,HTTP 错误需手动处理 |
| 超时设置 | 原生支持(xhr.timeout) | 需手动用 Promise.race 实现 |
| Cookie 发送 | 默认发送 | 默认不发送(需加 credentials) |
| 响应解析 | 需手动 JSON.parse | 内置 response.json () 等方法 |
| 浏览器兼容性 | 所有浏览器(包括 IE) | IE 不支持(需 polyfill) |
结论:现代项目优先使用 fetch(搭配 Promise/async/await),如需兼容 IE 则用 XHR 或 fetch polyfill。
四、JS 内存模型:异步执行的 “底层逻辑”
前面讲的 Ajax、Promise、fetch 都是 “异步用法”,但你有没有想过:为什么异步任务不会阻塞同步代码?Promise.then()的回调为什么在同步代码之后执行?这一切的答案,都藏在 JS 的内存模型和 Event Loop 中。
1. 内存模型核心组成
JS 内存模型主要分为三部分:调用栈(Call Stack) 、堆(Heap) 、任务队列(Task Queue) 。
- 调用栈:存放同步代码的执行上下文(函数调用),遵循 “后进先出”(LIFO)原则,同步代码按顺序入栈、执行、出栈。
- 堆:存放引用类型数据(如对象、数组、函数),因为引用类型大小不固定,无法放入栈中,堆是 “动态分配的内存区域”。
- 任务队列:存放异步任务的回调函数(如 Ajax 响应、setTimeout、Promise.then ()),分为 “宏任务队列” 和 “微任务队列”。
2. 关键机制:Event Loop(事件循环)
JS 是单线程的,同步代码优先执行,异步任务的执行依赖 Event Loop,流程如下:
- 同步代码依次进入调用栈执行,执行完后出栈。
- 异步任务(如 fetch 请求、setTimeout)执行时,不会