引言:异步编程的演进之路
在前端开发中,数据请求和延迟执行是最常见的两类异步操作。早期我们依赖 XMLHttpRequest(Ajax)配合回调函数处理网络请求,用 setTimeout 实现延时逻辑。然而,回调嵌套带来的“回调地狱”让代码难以维护。
ES6 引入的 Promise 彻底改变了这一局面——它将异步操作封装为可链式调用的对象,使代码更清晰、错误处理更统一。本文将通过三个核心示例:原生 Ajax vs fetch 对比、手写支持 Promise 的 getJSON 函数、实现 sleep 工具函数,深入理解 Promise 如何重构异步编程范式,并掌握其在实际项目中的封装技巧。
一、Ajax 与 fetch:异步请求的两种范式
1.1 原生 Ajax:基于事件的回调模型
传统 Ajax 使用 XMLHttpRequest 对象,通过监听 onreadystatechange 事件处理响应:
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://jsonplaceholder.typicode.com/users", true);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
这种方式存在明显缺陷:
- 逻辑分散:请求发起与响应处理分离
- 错误处理复杂:需分别检查
status和onerror - 无法链式调用:难以组合多个异步操作
1.2 fetch:基于 Promise 的现代方案
fetch 是浏览器原生提供的新一代 API,天然返回 Promise:
fetch('https://jsonplaceholder.typicode.com/users')
.then(res => res.json())
.then(data => console.log(data));
优势显著:
- 链式语法:
.then()串联操作 - 标准化:统一处理成功/失败(配合
.catch()) - 语义清晰:
res.json()显式解析 JSON
“fetch 简单易用,基于 Promise 实现,无需回调函数”。
但 fetch 并非万能——它不自动处理 HTTP 错误状态码(如 404、500),需手动判断 res.ok。因此,在需要精细控制的场景,封装自己的请求函数仍是必要技能。
二、手写 getJSON:用 Promise 封装 Ajax
2.1 为什么需要封装?
尽管 fetch 简洁,但在以下情况仍需自定义封装:
- 兼容旧浏览器(不支持 fetch)
- 统一错误处理逻辑
- 添加拦截器(如 Token 注入)
- 返回标准化数据结构
2.2 核心实现:Promise + XMLHttpRequest
function getJSON(url) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.send();
return new Promise((resolve, reject) => {
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
}
};
xhr.onerror = function() {
reject(new Error("网络错误"));
};
});
}
关键设计解析:
-
返回 Promise 实例
使函数具备.then()能力,符合“thenable”规范。 -
resolve/reject 控制状态流转
- 成功时:
resolve(解析后的 JSON)→ 触发.then() - 失败时:
reject(Error)→ 触发.catch()
- 成功时:
-
全面错误捕获
同时监听onreadystatechange(HTTP 错误)和onerror(网络中断)。
2.3 使用效果
getJSON("https://jsonplaceholder.typicode.com/users")
.then(data => console.log(data))
.catch(err => console.log(err));
代码结构与 fetch 几乎一致,但底层完全可控。这正是封装的价值:对外提供简洁接口,对内隐藏复杂实现。
三、手写 sleep:Promise 的延时控制艺术
3.1 需求场景
在测试、动画、流程控制中,常需“暂停”代码执行一段时间。传统做法:
setTimeout(() => {
console.log("2秒后执行");
}, 2000);
但 setTimeout 无法融入 Promise 链,导致逻辑割裂。
3.2 Promise 化 sleep 函数
function sleep(ms) {
return new Promise(resolve => {
setTimeout(() => {
resolve(); // 将状态从 pending → fulfilled
}, ms);
});
}
或更简洁的箭头函数版本:
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
状态流转机制:
- 初始状态:
pending(等待) setTimeout触发后:调用resolve()→ 状态变为fulfilled.then()回调立即执行
3.3 链式调用实战
sleep(2000)
.then(() => console.log("2秒后执行"))
.catch(err => console.log(err))
.finally(() => console.log("finally"));
.catch():捕获可能的异常(虽然 sleep 通常不会出错).finally():无论成功失败都会执行,适合清理操作
这种写法让延时操作无缝融入异步流程,例如:
loadData() .then(data => process(data)) .then(() => sleep(1000)) .then(() => showResult());
四、Promise 核心机制再理解
根据笔记,Promise 的本质是异步流程控制的状态机:
| 状态 | 触发条件 | 后续行为 |
|---|---|---|
pending(等待) | Promise 刚创建 | 可转向 fulfilled 或 rejected |
fulfilled(已完成) | 调用 resolve() | 执行 .then() 回调 |
rejected(已拒绝) | 调用 reject() 或抛出异常 | 执行 .catch() 回调 |
关键特性:
- 状态不可逆:一旦变为 fulfilled/rejected,无法再改变
- executor 同步执行:传给
new Promise()的函数会立即运行 - 异步任务放入回调:真正的耗时操作(如网络请求、定时器)放在 executor 内部
五、现代异步编程的最佳实践
5.1 优先使用 Promise 封装异步操作
无论是网络请求、文件读取还是定时任务,都应封装为返回 Promise 的函数。这带来三大好处:
- 统一接口:所有异步操作都可用
.then()处理 - 组合能力:配合
Promise.all()、Promise.race()实现并发控制 - 兼容 async/await:未来可无缝升级到更简洁的语法
5.2 错误处理必须全覆盖
- 网络请求:检查 HTTP 状态码 + 网络错误
- JSON 解析:
try/catch防止格式错误 - 用户输入:验证参数合法性
5.3 工具函数库化
将 getJSON、sleep 等通用函数放入工具库(如 utils.js),实现复用:
// utils.js
export const getJSON = (url) => { /* ... */ };
export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
结语:从手写到掌控
通过手写 getJSON 和 sleep,我们不仅掌握了 Promise 的封装技巧,更理解了其背后的设计哲学:将异步操作转化为可组合、可预测的状态流。
Ajax 是历史的基石,fetch 是现代的标准,而 Promise 是连接两者的桥梁。在 async/await 已成主流的今天,深入理解 Promise 仍是必要的——因为所有高级语法,终将回归到这个“事实标准”的状态机模型。
掌握它,就掌握了现代 JavaScript 异步编程的灵魂。