引言
在现代前端开发中,异步请求是再常见不过的操作。早期我们依赖 Ajax(XMLHttpRequest) 发起网络请求,而如今更推荐使用基于 Promise 的 fetch API。但理解底层原理、掌握如何将传统回调式 API 封装为 Promise 形式,依然是面试和工程实践中非常重要的能力。
本文将带你深入理解:
- Ajax 的工作原理与状态机制
- 同步 vs 异步请求的本质区别
- Promise 如何解决回调复杂性
- 如何手写一个支持 Promise 的
getJSON函数 - 并附带一个实用的
sleep函数实现
第一章:深入理解 Ajax —— 手动发起 HTTP 请求
在前端开发中,我们常常需要从服务器获取数据并动态更新页面,而无需刷新整个网页。这种能力的核心技术之一就是 Ajax(Asynchronous JavaScript and XML) 。
虽然名字里有 “XML”,但如今我们几乎总是用 JSON 作为数据格式。Ajax 的本质是使用浏览器内置的 XMLHttpRequest 对象,主动向服务器发起 HTTP 请求,并在收到响应后通过 JavaScript 操作 DOM 来更新界面。
1.1 一个简单的同步 Ajax 示例
让我们先看一段代码,它使用 XMLHttpRequest 同步请求 GitHub 上某个组织的成员列表:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ajax 请求</title>
</head>
<body>
<ul id="members"></ul>
<script>
// 创建 XMLHttpRequest 实例
const xhr = new XMLHttpRequest();
console.log(xhr.readyState, '------'); // 输出: 0 (UNSENT)
// 初始化请求:GET 方法,目标 URL,同步(false)
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', false);
console.log(xhr.readyState, '|------'); // 输出: 1 (OPENED)
// 发送请求(同步模式下会阻塞后续代码,直到响应返回)
xhr.send();
console.log(xhr.readyState, '|---|---'); // 输出: 4 (DONE)
// 直接使用响应数据(因为是同步,此时响应已就绪)
console.log(xhr.responseText);
// 解析 JSON 并更新页面
const data = JSON.parse(xhr.responseText);
document.getElementById('members').innerHTML = data.map(member =>
`<li>${member.login}</li>`
).join('');
</script>
</body>
</html>
这段代码能正常工作,但它使用了 同步请求(false) ,这在现代 Web 开发中已被弃用且不推荐使用。
⚠️ 为什么?
同步请求会阻塞主线程,导致页面“卡死”——用户无法点击、滚动或交互,直到请求完成。这在慢网络下体验极差,甚至可能被浏览器警告或中断。
1.2 Ajax 的五个状态(readyState)
XMLHttpRequest 对象有一个关键属性:readyState,它表示请求的当前阶段,共有 5 个值:
| 值 | 状态名 | 含义 |
|---|---|---|
| 0 | UNSENT | open() 尚未调用 |
| 1 | OPENED | open() 已调用,连接已建立 |
| 2 | HEADERS_RECEIVED | send() 已调用,响应头已接收 |
| 3 | LOADING | 响应体正在加载中(部分数据可用) |
| 4 | DONE | 请求完成,响应数据全部接收 |
在上面的同步示例中,我们看到:
- 初始为
0 - 调用
open()后变为1 - 调用
send()并等待完成后直接跳到4
但在异步模式下,状态是逐步变化的,我们需要监听这个变化。
1.3 推荐做法:使用异步 Ajax(事件驱动)
正确的 Ajax 写法应使用异步请求(true) ,并通过监听 onreadystatechange 事件来处理响应:
const xhr = new XMLHttpRequest();
// 异步请求(true 是默认值,可省略)
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
// 监听状态变化
xhr.onreadystatechange = function () {
// 只有当请求完成(readyState === 4)且 HTTP 状态成功(200)时才处理数据
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
document.getElementById('members').innerHTML = data.map(member =>
`<li>${member.login}</li>`
).join('');
} else {
console.error('请求失败:', xhr.status, xhr.statusText);
}
}
};
// 捕获网络级错误(如断网、DNS 失败)
xhr.onerror = function () {
console.error('网络错误:无法连接到服务器');
};
// 发送请求(不会阻塞后续代码)
xhr.send();
✅ 优势:
- 页面保持响应,用户体验流畅
- 错误处理更全面(HTTP 错误 + 网络错误)
- 符合现代 Web 最佳实践
1.4 Ajax 的局限性
尽管 Ajax 强大,但它也有缺点:
- 语法繁琐:需手动管理状态、解析 JSON、处理错误
- 回调地狱风险:多个请求嵌套时代码难以维护
- 不支持 Promise:无法直接使用
.then()或async/await
正因如此,ES6 引入了 Promise,而浏览器也提供了更现代化的 fetch API。但我们仍需理解 Ajax,因为它:
- 是所有异步请求的底层基础
- 在老项目或特殊场景(如上传进度监听)中仍有价值
- 是封装自定义请求工具的前提
🔜 在下一章,我们将把这段 Ajax 逻辑封装成一个返回 Promise 的
getJSON函数,让异步请求变得更简洁、更可控。
第二章:Promise —— 异步流程控制的事实标准
为了解决回调函数带来的复杂性和可读性问题,ES6 引入了 Promise。它是一种用于表示异步操作最终完成或失败的对象。
2.1 Promise 的三种状态
pending(初始状态,既不是成功也不是失败)fulfilled(操作成功完成,调用resolve)rejected(操作失败,调用reject)
一旦状态改变,就不可逆。
2.2 基本用法
new Promise((resolve, reject) => {
// executor 函数:立即同步执行
if (/* 异步操作成功 */) {
resolve(data);
} else {
reject(error);
}
})
.then(data => { /* 成功处理 */ })
.catch(err => { /* 失败处理 */ })
.finally(() => { /* 无论成败都执行,如关闭 loading */ });
💡 关键点:Promise 让异步代码“看起来像同步”,极大提升可读性和可维护性。
Promise异步任务同步化详解请看:Promise:让 JavaScript 异步任务“同步化”的利器 - 掘金
第三章:手写 getJSON —— 用 Promise 封装 Ajax
我们的目标是实现一个函数 getJSON(url),它:
- 发起 GET 请求
- 自动解析 JSON 响应
- 返回 Promise 实例,支持
.then()/.catch()
最终实现(带详细注释)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>getJSON 方法手写</title>
</head>
<body>
<script>
/**
* 手写 getJSON 函数:基于 Ajax + Promise 封装
* @param {string} url - 请求地址
* @returns {Promise} - 解析后的 JSON 数据 或 错误信息
*/
function getJSON(url) {
// 直接返回一个 Promise 实例
return new Promise((resolve, reject) => {
// 1. 创建 XMLHttpRequest 对象(Ajax 核心)
const xhr = new XMLHttpRequest();
// 2. 初始化请求:GET 方法,异步(true)
xhr.open('GET', url, true);
// 3. 设置 onreadystatechange 监听器
xhr.onreadystatechange = function () {
// readyState === 4 表示请求已完成
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
// 4. 尝试将响应文本解析为 JSON 对象
const data = JSON.parse(xhr.responseText);
resolve(data); // 成功:转为 fulfilled 状态
} catch (e) {
// 如果 JSON 解析失败,也视为错误
reject(new Error('Invalid JSON response'));
}
} else {
// HTTP 状态码非 2xx,视为请求失败
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
}
};
// 5. 捕获网络错误(如 DNS 失败、断网等)
xhr.onerror = function () {
reject(new Error('Network request failed'));
};
// 6. 发送请求
xhr.send();
});
}
// 使用示例:获取 GitHub 用户信息
getJSON('https://api.github.com/users/shunwuyu')
.then(data => {
console.log('用户数据:', data);
// 输出:{ login: "shunwuyu", id: 640278, ... }
})
.catch(err => {
console.error('请求出错:', err.message);
});
</script>
</body>
</html>
关键改进说明:
-
错误处理更全面:
- 区分 HTTP 错误(如 404)和网络错误(
onerror) - 增加
JSON.parse的try...catch,防止无效 JSON 导致崩溃
- 区分 HTTP 错误(如 404)和网络错误(
-
符合 Promise 规范:
- 成功时
resolve(data) - 失败时
reject(error)
- 成功时
-
无副作用:
- 不修改全局状态,纯函数式封装
第四章:Bonus —— 手写 sleep 函数(Promise 版)
有时我们需要“暂停”代码执行(如模拟延迟、节流测试),可以用 setTimeout + Promise 实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>手写 sleep 函数</title>
</head>
<body>
<script>
/**
* sleep 函数:延迟指定毫秒后 resolve
* @param {number} ms - 延迟时间(毫秒)
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(); // 延迟结束后变为 fulfilled 状态
}, ms);
});
}
// 使用示例
sleep(3000)
.then(() => {
console.log('3秒后执行!'); // 3秒后输出
});
// 也可配合 async/await 使用
async function demo() {
await sleep(2000);
console.log('2秒后执行(async/await)');
}
demo();
</script>
</body>
</html>
第五章:总结与思考
通过本文,我们完成了从原始 Ajax 到 Promise 封装的完整演进:
| 阶段 | 特点 | 问题 |
|---|---|---|
| 同步 Ajax | 代码简单,直接取值 | 阻塞主线程,用户体验差 |
| 异步 Ajax | 非阻塞,事件驱动 | 回调嵌套,错误处理分散 |
| Promise 封装 | 链式调用,统一错误处理 | 需要理解 Promise 机制 |
| fetch / async-await | 更现代、简洁 | 需要 polyfill 兼容旧浏览器 |
为什么还要学 Ajax?
- 面试高频考点:考察对异步、HTTP、浏览器 API 的理解
- 调试能力:当 fetch 行为异常时,知道底层发生了什么
- 定制需求:如监听上传进度、取消请求等,仍需 XMLHttpRequest
小练习建议
- 扩展
getJSON支持超时(timeout) - 添加请求头(如
Authorization) - 支持 POST 方法和 body 参数
- 封装成通用
request(options)函数
兼容性提示:本文代码适用于现代浏览器(Chrome/Firefox/Edge/Safari),IE 需 Polyfill Promise。