从 Ajax 到 Promise:手写异步工具函数的现代实践

65 阅读5分钟

引言:异步编程的演进之路

在前端开发中,数据请求延迟执行是最常见的两类异步操作。早期我们依赖 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);
    }
};

这种方式存在明显缺陷:

  • 逻辑分散:请求发起与响应处理分离
  • 错误处理复杂:需分别检查 statusonerror
  • 无法链式调用:难以组合多个异步操作

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("网络错误"));
        };
    });
}

关键设计解析:

  1. 返回 Promise 实例
    使函数具备 .then() 能力,符合“thenable”规范。

  2. resolve/reject 控制状态流转

    • 成功时:resolve(解析后的 JSON) → 触发 .then()
    • 失败时:reject(Error) → 触发 .catch()
  3. 全面错误捕获
    同时监听 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 的函数。这带来三大好处:

  1. 统一接口:所有异步操作都可用 .then() 处理
  2. 组合能力:配合 Promise.all()Promise.race() 实现并发控制
  3. 兼容 async/await:未来可无缝升级到更简洁的语法

5.2 错误处理必须全覆盖

  • 网络请求:检查 HTTP 状态码 + 网络错误
  • JSON 解析:try/catch 防止格式错误
  • 用户输入:验证参数合法性

5.3 工具函数库化

getJSONsleep 等通用函数放入工具库(如 utils.js),实现复用:

// utils.js
export const getJSON = (url) => { /* ... */ };
export const sleep = (ms) => new Promise(r => setTimeout(r, ms));

结语:从手写到掌控

通过手写 getJSONsleep,我们不仅掌握了 Promise 的封装技巧,更理解了其背后的设计哲学:将异步操作转化为可组合、可预测的状态流

Ajax 是历史的基石,fetch 是现代的标准,而 Promise 是连接两者的桥梁。在 async/await 已成主流的今天,深入理解 Promise 仍是必要的——因为所有高级语法,终将回归到这个“事实标准”的状态机模型。

掌握它,就掌握了现代 JavaScript 异步编程的灵魂。