从 Ajax 到 fetch:解锁 JS 异步通信 + 内存模型核心逻辑

90 阅读6分钟

在现代 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 繁琐:配置请求、监听状态、解析数据步骤分散,代码冗余。
  • 错误处理不统一:需要手动判断readyStatestatus,异常捕获复杂。

这些痛点,催生了 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
底层 APIXMLHttpRequest 对象浏览器原生 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,流程如下:

  1. 同步代码依次进入调用栈执行,执行完后出栈。
  2. 异步任务(如 fetch 请求、setTimeout)执行时,不会