手写 getJSON:从 Ajax 到 Promise 的优雅封装

50 阅读6分钟

引言

在现代前端开发中,异步请求是再常见不过的操作。早期我们依赖 Ajax(XMLHttpRequest) 发起网络请求,而如今更推荐使用基于 Promisefetch 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 个值:

状态名含义
0UNSENTopen() 尚未调用
1OPENEDopen() 已调用,连接已建立
2HEADERS_RECEIVEDsend() 已调用,响应头已接收
3LOADING响应体正在加载中(部分数据可用)
4DONE请求完成,响应数据全部接收

在上面的同步示例中,我们看到:

  • 初始为 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>

关键改进说明:

  1. 错误处理更全面

    • 区分 HTTP 错误(如 404)和网络错误(onerror
    • 增加 JSON.parsetry...catch,防止无效 JSON 导致崩溃
  2. 符合 Promise 规范

    • 成功时 resolve(data)
    • 失败时 reject(error)
  3. 无副作用

    • 不修改全局状态,纯函数式封装

第四章: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

小练习建议

  1. 扩展 getJSON 支持超时(timeout
  2. 添加请求头(如 Authorization
  3. 支持 POST 方法和 body 参数
  4. 封装成通用 request(options) 函数

兼容性提示:本文代码适用于现代浏览器(Chrome/Firefox/Edge/Safari),IE 需 Polyfill Promise。