深入理解 Ajax 异步请求:从 XMLHttpRequest 到 Node.js HTTP 服务实践

深入理解 Ajax 异步请求:从 XMLHttpRequest 到 Node.js HTTP 服务实践

  • 前言
  • [一、什么是 Ajax](#一、什么是 Ajax)
    • [1.1 传统网页的工作方式](#1.1 传统网页的工作方式)
    • [1.2 Ajax 的出现](#1.2 Ajax 的出现)
  • [二、JavaScript 为什么需要异步](#二、JavaScript 为什么需要异步)
    • [2.1 JavaScript 是单线程语言](#2.1 JavaScript 是单线程语言)
    • [2.2 如果没有异步会发生什么](#2.2 如果没有异步会发生什么)
    • [2.3 同步与异步](#2.3 同步与异步)
  • [三、Event Loop 事件循环机制](#三、Event Loop 事件循环机制)
    • [3.1 执行过程分析](#3.1 执行过程分析)
    • [3.2 Event Loop 工作流程](#3.2 Event Loop 工作流程)
    • [3.3 Ajax 与 Event Loop 的关系](#3.3 Ajax 与 Event Loop 的关系)
  • [四、JSON 数据格式](#四、JSON 数据格式)
    • [4.1 JSON 为什么存在](#4.1 JSON 为什么存在)
    • [4.2 JSON.stringify()](#4.2 JSON.stringify())
    • [4.3 replace 参数](#4.3 replace 参数)
    • [4.4 space 参数](#4.4 space 参数)
    • [4.5 JSON.parse()](#4.5 JSON.parse())
    • [4.6 stringify 与 parse 的对应关系](#4.6 stringify 与 parse 的对应关系)
  • [五、使用 Node.js 搭建 HTTP 服务](#五、使用 Node.js 搭建 HTTP 服务)
    • [5.1 Node.js 内置 HTTP 模块](#5.1 Node.js 内置 HTTP 模块)
    • [5.2 创建 HTTP 服务器](#5.2 创建 HTTP 服务器)
      • [createServer 的工作原理](#createServer 的工作原理)
    • [5.3 req 与 res](#5.3 req 与 res)
  • [六、模拟 Todo 数据](#六、模拟 Todo 数据)
  • 七、实现路由功能
    • [7.1 什么是路由](#7.1 什么是路由)
    • [7.2 首页路由](#7.2 首页路由)
  • [八、实现 Todo 接口](#八、实现 Todo 接口)
    • [8.1 为什么需要响应头](#8.1 为什么需要响应头)
  • 九、解决跨域问题(CORS)
    • [9.1 什么是同源策略](#9.1 什么是同源策略)
    • [9.2 Access-Control-Allow-Origin](#9.2 Access-Control-Allow-Origin)
  • 十、设置响应数据类型
  • [十一、返回 Todo 数据](#十一、返回 Todo 数据)
    • [11.1 为什么不能直接返回对象](#11.1 为什么不能直接返回对象)
    • [11.2 数据序列化过程](#11.2 数据序列化过程)
  • 十二、监听端口
    • [12.1 什么是端口](#12.1 什么是端口)
  • 十三、完整服务端代码
  • [十四、使用 XMLHttpRequest 发起 Ajax 请求](#十四、使用 XMLHttpRequest 发起 Ajax 请求)
  • 十五、页面结构分析
    • [15.1 Todos 容器](#15.1 Todos 容器)
  • 十六、事件注册机制
    • [16.1 为什么需要事件监听](#16.1 为什么需要事件监听)
    • [16.2 addEventListener 参数分析](#16.2 addEventListener 参数分析)
  • [十七、Ajax 的发展历史](#十七、Ajax 的发展历史)
    • [17.1 Web 1.0 时代](#17.1 Web 1.0 时代)
    • [17.2 Ajax 时代](#17.2 Ajax 时代)
    • [17.3 Fetch 时代](#17.3 Fetch 时代)
  • [十八、创建 XMLHttpRequest 对象](#十八、创建 XMLHttpRequest 对象)
    • [18.1 XMLHttpRequest 是什么](#18.1 XMLHttpRequest 是什么)
  • 十九、配置请求
    • [19.1 第一个参数](#19.1 第一个参数)
    • [19.2 第二个参数](#19.2 第二个参数)
    • [19.3 第三个参数](#19.3 第三个参数)
  • [二十、send() 发送请求](#二十、send() 发送请求)
  • 二十一、请求状态监听
    • [21.1 什么是 onreadystatechange](#21.1 什么是 onreadystatechange)
  • [二十二、readyState 状态详解](#二十二、readyState 状态详解)
  • [二十三、HTTP 状态码](#二十三、HTTP 状态码)
  • 二十四、解析服务器返回的数据
    • [24.1 responseText 是什么](#24.1 responseText 是什么)
    • [24.2 JSON.parse](#24.2 JSON.parse)
  • 二十五、动态渲染页面
  • 二十六、完整请求流程分析
    • [26.1 页面加载](#26.1 页面加载)
    • [26.2 创建 XMLHttpRequest 对象](#26.2 创建 XMLHttpRequest 对象)
    • [26.3 配置请求](#26.3 配置请求)
    • [26.4 注册状态监听](#26.4 注册状态监听)
    • [26.5 发送请求](#26.5 发送请求)
    • [26.6 Node服务器接收请求](#26.6 Node服务器接收请求)
    • [26.7 浏览器收到响应](#26.7 浏览器收到响应)
    • [26.8 解析 JSON 数据](#26.8 解析 JSON 数据)
    • [26.9 更新页面](#26.9 更新页面)
  • [二十七、为什么 Ajax 能实现局部刷新](#二十七、为什么 Ajax 能实现局部刷新)
  • [二十八、Fetch:现代 Ajax 方案](#二十八、Fetch:现代 Ajax 方案)
  • [二十九、Async 与 Await](#二十九、Async 与 Await)
  • 总结

前言

在现代 Web 开发中,前后端数据交互是最基础也是最重要的能力之一。当我们打开一个网页时,页面中的数据通常并不是直接写死在 HTML 中,而是通过网络请求从服务器动态获取。

例如:

  • 待办事项列表
  • 用户信息
  • 商品数据
  • 新闻内容

这些数据往往存储在服务器或数据库中,需要浏览器主动发起请求,再由服务器返回对应的数据。

如果每获取一次数据都重新加载整个页面,那么用户体验将会非常差。因此,Ajax 技术应运而生。

本文将通过一个完整的 TodoList 案例,从 HTTP 服务搭建开始,逐步讲解:

  • Ajax 的核心思想
  • JavaScript 异步机制
  • Event Loop 事件循环
  • JSON 数据格式
  • Node.js HTTP 服务
  • XMLHttpRequest 工作原理
  • DOM 动态渲染
  • Fetch 与 Async/Await

帮助大家建立完整的前后端通信知识体系。


一、什么是 Ajax

Ajax 全称:

text 复制代码
Asynchronous JavaScript And XML

中文通常翻译为:

text 复制代码
异步 JavaScript 与 XML

虽然名字中带有 XML,但现代开发中几乎已经全面使用 JSON 作为数据交换格式。

Ajax 的核心思想非常简单:

text 复制代码
页面不刷新

↓

发送网络请求

↓

获取服务器数据

↓

局部更新页面

1.1 传统网页的工作方式

早期 Web 页面采用的是同步刷新模式。

例如点击一个超链接:

html 复制代码
<a href="/news">新闻列表</a>

执行流程如下:

text 复制代码
用户点击链接

↓

浏览器发送请求

↓

服务器返回新页面

↓

浏览器重新加载整个页面

即使只是更新一小部分内容,也必须重新加载整个页面。

这种方式存在两个明显问题:

  • 页面闪烁
  • 用户体验较差

1.2 Ajax 的出现

Ajax 技术出现后,浏览器获得了主动向服务器发送请求的能力。

执行流程变成:

text 复制代码
用户操作页面

↓

发送异步请求

↓

服务器返回数据

↓

JavaScript更新DOM

↓

页面局部刷新

例如:

用户点击查看待办事项:

text 复制代码
点击按钮

↓

请求 /todos

↓

获取JSON数据

↓

更新列表区域

↓

页面其余部分保持不变

这也是现代 Web 应用能够实现流畅交互的重要原因。


二、JavaScript 为什么需要异步

理解 Ajax 之前,必须先理解 JavaScript 的执行机制。


2.1 JavaScript 是单线程语言

JavaScript 只有一个主线程。

所谓单线程,就是:

text 复制代码
同一时刻只能执行一个任务

例如:

javascript 复制代码
console.log("A");

console.log("B");

console.log("C");

输出结果:

text 复制代码
A
B
C

所有代码严格按照顺序执行。


2.2 如果没有异步会发生什么

假设浏览器发送一个网络请求:

javascript 复制代码
const data = request();

服务器需要 5 秒钟才能返回结果。

如果 JavaScript 采用同步执行模式,那么:

text 复制代码
请求发送

↓

等待5秒

↓

收到数据

↓

继续执行

在等待期间:

  • 页面无法响应
  • 按钮无法点击
  • 用户无法操作

整个浏览器都会被阻塞。

显然这是无法接受的。

因此 JavaScript 引入了异步机制。


2.3 同步与异步

同步执行:

javascript 复制代码
console.log(1);
console.log(2);
console.log(3);

输出:

text 复制代码
1
2
3

异步执行:

javascript 复制代码
console.log(1);

setTimeout(() => {
    console.log(2);
}, 0);

console.log(3);

输出:

text 复制代码
1
3
2

这里的关键问题是:

为什么明明先写了 setTimeout,却最后执行?

答案就在 Event Loop 中。


三、Event Loop 事件循环机制

JavaScript 并不会等待异步任务完成。

例如:

javascript 复制代码
console.log("start");

setTimeout(() => {
    console.log("timeout");
}, 1000);

console.log("end");

输出:

text 复制代码
start
end
timeout

3.1 执行过程分析

第一步:

javascript 复制代码
console.log("start");

输出:

text 复制代码
start

第二步:

javascript 复制代码
setTimeout(...)

发现是异步任务。

JavaScript 不会等待。

而是交给浏览器处理。


第三步:

继续执行后续代码:

javascript 复制代码
console.log("end");

输出:

text 复制代码
end

第四步:

1 秒后计时结束。

回调函数进入任务队列。


第五步:

Event Loop 检测调用栈为空。

将回调函数取出执行。

输出:

text 复制代码
timeout

3.2 Event Loop 工作流程

整个过程如下:

text 复制代码
主线程(Call Stack)

↓

遇到异步任务

↓

交给浏览器(Web APIs)

↓

任务完成

↓

进入任务队列(Task Queue)

↓

Event Loop检测

↓

重新进入主线程执行

可以简化表示为:

text 复制代码
Call Stack
    ↓
Web APIs
    ↓
Task Queue
    ↓
Event Loop
    ↓
Call Stack

3.3 Ajax 与 Event Loop 的关系

Ajax 请求本质也是异步任务。

例如:

javascript 复制代码
xhr.send();

发送请求后:

text 复制代码
浏览器开始请求服务器

↓

JavaScript继续执行

↓

服务器返回数据

↓

触发回调函数

↓

处理响应结果

因此:

text 复制代码
发送请求
≠
等待响应

这一点非常重要。

很多初学者误以为:

javascript 复制代码
xhr.send();

执行后程序会停下来等待服务器返回。

实际上并不会。

JavaScript 会继续执行后面的代码。


四、JSON 数据格式

浏览器和服务器之间交换数据时,最常使用的数据格式就是 JSON。

JSON 全称:

text 复制代码
JavaScript Object Notation

即:

text 复制代码
JavaScript对象表示法

4.1 JSON 为什么存在

例如:

javascript 复制代码
const todo = {
    id:1,
    title:"过四六级",
    completed:false
};

这是一个 JavaScript 对象。

但是网络无法直接传输对象。

HTTP 能传输的本质上是:

text 复制代码
字符串

或

二进制数据

因此需要将对象转换为字符串。

这个过程称为:

text 复制代码
序列化(Serialization)

4.2 JSON.stringify()

作用:

text 复制代码
JavaScript对象

↓

JSON字符串

例如:

javascript 复制代码
const todo = {
    id:1,
    title:"过四六级"
};

console.log(
    JSON.stringify(todo)
);

输出:

json 复制代码
{"id":1,"title":"过四六级"}

此时对象已经变成字符串,可以通过网络发送。


4.3 replace 参数

语法:

javascript 复制代码
JSON.stringify(value, replace)

用于控制哪些属性参与序列化。

例如:

javascript 复制代码
const todo = {
    id:1,
    title:"过四六级",
    completed:false
};

console.log(
    JSON.stringify(
        todo,
        ["title"]
    )
);

输出:

json 复制代码
{"title":"过四六级"}

只有 title 被保留。


replace 还可以接收函数:

javascript 复制代码
JSON.stringify(
    todo,
    function(key,value){
        return value;
    }
);

这种写法可以对序列化过程进行更加灵活的控制。


4.4 space 参数

语法:

javascript 复制代码
JSON.stringify(
    value,
    null,
    space
);

用于控制缩进。

例如:

javascript 复制代码
JSON.stringify(todo,null,2);

输出:

json 复制代码
{
  "id": 1,
  "title": "过四六级"
}

开发阶段经常使用:

javascript 复制代码
JSON.stringify(data,null,2);

因为格式更加清晰。

但在实际网络传输时通常不会添加缩进。

原因是:

text 复制代码
缩进越多

↓

字符串越长

↓

传输体积越大

4.5 JSON.parse()

作用:

text 复制代码
JSON字符串

↓

JavaScript对象

例如:

javascript 复制代码
const str =
'{"id":1,"title":"过四六级"}';

const obj =
JSON.parse(str);

console.log(obj.title);

输出:

text 复制代码
过四六级

4.6 stringify 与 parse 的对应关系

服务器发送数据:

text 复制代码
对象

↓

JSON.stringify()

↓

JSON字符串

↓

网络传输

浏览器接收数据:

text 复制代码
JSON字符串

↓

JSON.parse()

↓

对象

这是前后端通信过程中最基础也是最重要的一组 API。


五、使用 Node.js 搭建 HTTP 服务

在了解 Ajax 请求原理之前,我们首先需要拥有一个能够提供数据的服务器。

在实际开发中,前端页面的数据通常来自:

  • MySQL
  • PostgreSQL
  • MongoDB
  • Redis

等数据库。

但为了更专注于理解 HTTP 通信过程,本文使用 Node.js 原生 HTTP 模块模拟后端接口。


5.1 Node.js 内置 HTTP 模块

Node.js 提供了内置的 http 模块。

无需安装即可直接使用。

javascript 复制代码
const http = require("http");

什么是模块(Module)

随着项目规模增大,不可能将所有代码都写在一个文件中。

例如:

text 复制代码
project
│
├── app.js
├── user.js
├── todo.js
└── utils.js

每个文件负责不同功能。

这种开发方式称为:

text 复制代码
模块化开发

CommonJS 模块规范

Node.js 早期采用 CommonJS 规范。

导入模块:

javascript 复制代码
const http = require("http");

导出模块:

javascript 复制代码
module.exports = data;

ES Module 模块规范

现代 JavaScript 推荐使用:

javascript 复制代码
import http from "http";

导出:

javascript 复制代码
export default data;

对应关系如下:

CommonJS ES Module
require import
module.exports export
Node早期方案 ECMAScript官方方案

5.2 创建 HTTP 服务器

代码如下:

javascript 复制代码
const http = require("http");

http.createServer((req,res)=>{

});

这里调用了:

javascript 复制代码
http.createServer()

用于创建一个 HTTP 服务。


createServer 的工作原理

服务器启动后:

text 复制代码
等待请求

当浏览器访问:

text 复制代码
http://localhost:3000

时:

text 复制代码
浏览器发送请求

↓

Node接收请求

↓

执行回调函数

因此:

javascript 复制代码
(req,res)=>{

}

并不是服务器启动时执行。

而是在收到请求时执行。

这一点需要特别注意。


5.3 req 与 res

createServer 回调函数有两个参数:

javascript 复制代码
(req,res)

req(Request)

表示请求对象。

保存客户端发送过来的信息。

例如:

javascript 复制代码
req.url

获取访问路径。


访问:

text 复制代码
http://localhost:3000/

得到:

javascript 复制代码
req.url
// "/"

访问:

text 复制代码
http://localhost:3000/todos

得到:

javascript 复制代码
req.url
// "/todos"

除了 url 之外,后续开发中还会经常使用:

javascript 复制代码
req.method

请求方式。

例如:

text 复制代码
GET
POST
PUT
DELETE

res(Response)

表示响应对象。

负责向浏览器返回数据。

例如:

javascript 复制代码
res.end()

表示:

text 复制代码
结束响应并返回数据

六、模拟 Todo 数据

为了模拟数据库查询结果。

我们定义一个数组:

javascript 复制代码
const todos = [
    {
        id:1,
        title:"过四六级",
        completed:false
    },
    {
        id:2,
        title:"回家过节",
        completed:false
    }
];

这里很多初学者容易误解。

实际上:

text 复制代码
这不是数据库

而只是:

text 复制代码
内存中的普通数组

用于模拟数据库返回的数据。

真实项目中通常是:

sql 复制代码
SELECT * FROM todos;

查询数据库后再返回结果。


七、实现路由功能

完整代码:

javascript 复制代码
if(req.url === "/"){
    res.end("hello world");
}

7.1 什么是路由

路由(Route)本质上就是:

text 复制代码
不同URL

↓

对应不同处理逻辑

例如:

text 复制代码
/

首页


text 复制代码
/todos

任务列表


text 复制代码
/users

用户列表


因此:

javascript 复制代码
if(req.url === "/"){
}

实际上就是最简单的路由实现。


7.2 首页路由

javascript 复制代码
if(req.url === "/"){
    res.end("hello world");
}

访问:

text 复制代码
http://localhost:3000/

浏览器显示:

text 复制代码
hello world

八、实现 Todo 接口

继续添加:

javascript 复制代码
if(req.url === "/todos"){

}

当用户访问:

text 复制代码
http://localhost:3000/todos

时进入对应逻辑。


8.1 为什么需要响应头

服务器返回数据时。

除了返回内容本身。

还会返回大量描述信息。

例如:

text 复制代码
内容类型
编码方式
缓存策略
跨域规则

这些信息称为:

text 复制代码
HTTP响应头

九、解决跨域问题(CORS)

代码:

javascript 复制代码
res.setHeader(
    "Access-Control-Allow-Origin",
    "*"
);

9.1 什么是同源策略

假设:

前端运行在:

text 复制代码
http://localhost:5500

后端运行在:

text 复制代码
http://localhost:3000

虽然都在本机。

但是:

text 复制代码
端口不同

浏览器认为:

text 复制代码
不是同一个源

因此会触发:

text 复制代码
跨域限制

浏览器默认禁止:

javascript 复制代码
fetch("http://localhost:3000/todos");

直接访问。

控制台会报错:

text 复制代码
CORS Error

9.2 Access-Control-Allow-Origin

为了解决跨域问题。

服务器需要主动告诉浏览器:

text 复制代码
允许访问

代码:

javascript 复制代码
res.setHeader(
    "Access-Control-Allow-Origin",
    "*"
);

表示:

text 复制代码
允许所有来源访问

开发阶段经常使用:

javascript 复制代码
"*"

生产环境通常会指定具体域名。

例如:

javascript 复制代码
res.setHeader(
    "Access-Control-Allow-Origin",
    "https://example.com"
);

安全性更高。


十、设置响应数据类型

代码:

javascript 复制代码
res.setHeader(
    "Content-Type",
    "application/json"
);

10.1 为什么需要 Content-Type

浏览器收到数据后。

必须知道:

text 复制代码
返回内容是什么类型

否则无法正确解析。


常见类型:

HTML

text 复制代码
text/html

普通文本

text 复制代码
text/plain

JSON

text 复制代码
application/json

图片

text 复制代码
image/png

视频

text 复制代码
video/mp4

因此:

javascript 复制代码
application/json

告诉浏览器:

text 复制代码
我返回的是JSON数据

十一、返回 Todo 数据

代码:

javascript 复制代码
res.end(
    JSON.stringify(todos)
);

11.1 为什么不能直接返回对象

很多初学者会尝试:

javascript 复制代码
res.end(todos);

实际上这是错误的。

原因在于:

HTTP 协议只能传输:

text 复制代码
字符串

或

二进制数据

而:

javascript 复制代码
[]
{}

属于 JavaScript 对象。

无法直接传输。


11.2 数据序列化过程

因此需要:

javascript 复制代码
JSON.stringify()

进行序列化。

转换过程:

javascript 复制代码
[
    {
        id:1,
        title:"过四六级"
    }
]

json 复制代码
[
    {
        "id":1,
        "title":"过四六级"
    }
]

text 复制代码
JSON字符串

text 复制代码
通过HTTP发送

十二、监听端口

代码:

javascript 复制代码
.listen(3000,()=>{
    console.log(
        "server is running on 3000 port"
    );
});

12.1 什么是端口

可以把:

text 复制代码
IP地址

理解为:

text 复制代码
一栋大楼

把:

text 复制代码
Port

理解为:

text 复制代码
房间号

例如:

text 复制代码
127.0.0.1:3000

表示:

text 复制代码
本机

↓

3000号端口

访问:

text 复制代码
http://localhost:3000

实际上就是访问:

text 复制代码
本机3000端口上的HTTP服务

十三、完整服务端代码

javascript 复制代码
const http = require("http");

http.createServer((req,res)=>{

    const todos = [
        {
            id:1,
            title:"过四六级",
            completed:false
        },
        {
            id:2,
            title:"回家过节",
            completed:false
        }
    ];

    if(req.url === "/"){
        res.end("hello world");
    }

    if(req.url === "/todos"){

        res.setHeader(
            "Access-Control-Allow-Origin",
            "*"
        );

        res.setHeader(
            "Content-Type",
            "application/json"
        );

        res.end(
            JSON.stringify(todos)
        );
    }

}).listen(3000,()=>{
    console.log(
        "server is running on 3000 port"
    );
});

十四、使用 XMLHttpRequest 发起 Ajax 请求

服务端接口已经搭建完成。

接下来,我们开始编写前端代码,通过 Ajax 获取服务器中的 Todo 数据。

完整代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <ul id="todos"></ul>

    <button id="btn">按钮</button>

    <script>

        console.log("start");

        document
            .getElementById("btn")
            .addEventListener("click",function(){
                console.log("点击按钮");
            });

        const xhr = new XMLHttpRequest();

        xhr.open(
            "GET",
            "http://localhost:3000/todos",
            true
        );

        console.log("start");

        xhr.onreadystatechange = function(){

            console.log(xhr.readyState);

            if(
                xhr.status === 200 &&
                xhr.readyState === 4
            ){

                const todos =
                    JSON.parse(
                        xhr.responseText
                    );

                document
                    .getElementById("todos")
                    .innerHTML =
                    todos
                        .map(todo =>
                            `<li>${todo.title}</li>`
                        )
                        .join("");

            }

        }

        console.log("end");

        xhr.send();

    </script>

</body>
</html>

十五、页面结构分析

首先观察页面结构:

html 复制代码
<ul id="todos"></ul>

<button id="btn">按钮</button>

页面中只有两个元素:

一个用于显示任务列表:

html 复制代码
<ul id="todos"></ul>

一个用于演示事件机制:

html 复制代码
<button id="btn">按钮</button>

15.1 Todos 容器

初始状态:

html 复制代码
<ul id="todos"></ul>

页面中没有任何数据。


当服务器返回数据后:

html 复制代码
<ul id="todos">
    <li>过四六级</li>
    <li>回家过节</li>
</ul>

内容会动态生成。

这正是 Ajax 的核心思想:

text 复制代码
数据驱动页面

而不是:

text 复制代码
把数据写死在HTML中

十六、事件注册机制

代码:

javascript 复制代码
document
.getElementById("btn")
.addEventListener("click",function(){
    console.log("点击按钮");
});

16.1 为什么需要事件监听

浏览器无法提前知道:

text 复制代码
用户什么时候点击按钮

因此需要提前注册事件。

执行流程:

text 复制代码
注册事件

↓

等待用户操作

↓

事件触发

↓

执行回调函数

16.2 addEventListener 参数分析

第一个参数:

javascript 复制代码
"click"

表示事件类型。


常见事件:

javascript 复制代码
"click"

点击事件


javascript 复制代码
"input"

输入事件


javascript 复制代码
"change"

内容改变事件


javascript 复制代码
"submit"

表单提交事件


第二个参数:

javascript 复制代码
function(){

}

回调函数。

只有事件发生时才会执行。


十七、Ajax 的发展历史

在学习 XMLHttpRequest 之前。

有必要了解 Ajax 技术的发展过程。


17.1 Web 1.0 时代

早期网页主要依赖:

html 复制代码
<a href="">

和:

html 复制代码
<form>

进行页面跳转。

执行流程:

text 复制代码
点击链接

↓

浏览器发送请求

↓

服务器返回新页面

↓

浏览器重新刷新

整个页面都会被重新加载。


17.2 Ajax 时代

XMLHttpRequest 出现后。

浏览器获得了主动请求服务器的能力。

执行流程变成:

text 复制代码
发送请求

↓

获取数据

↓

更新页面局部区域

↓

无需刷新页面

这就是 Ajax 的本质。


17.3 Fetch 时代

现代浏览器逐渐采用:

javascript 复制代码
fetch()

代替:

javascript 复制代码
XMLHttpRequest

虽然 API 不同。

但核心思想完全一致:

text 复制代码
发送HTTP请求

↓

获取服务器数据

↓

更新页面

十八、创建 XMLHttpRequest 对象

代码:

javascript 复制代码
const xhr = new XMLHttpRequest();

18.1 XMLHttpRequest 是什么

可以理解为:

text 复制代码
浏览器中的HTTP客户端

专门负责:

  • 建立连接
  • 发送请求
  • 接收响应

创建对象后:

javascript 复制代码
xhr

就拥有了完整的 HTTP 通信能力。


十九、配置请求

代码:

javascript 复制代码
xhr.open(
    "GET",
    "http://localhost:3000/todos",
    true
);

19.1 第一个参数

javascript 复制代码
"GET"

请求方式。


常见请求方式:

GET

获取数据

javascript 复制代码
GET /todos

POST

创建数据

javascript 复制代码
POST /todos

PUT

修改数据

javascript 复制代码
PUT /todos/1

DELETE

删除数据

javascript 复制代码
DELETE /todos/1

19.2 第二个参数

javascript 复制代码
"http://localhost:3000/todos"

请求地址。

对应服务端:

javascript 复制代码
if(req.url === "/todos")

19.3 第三个参数

javascript 复制代码
true

表示异步请求。


如果写成:

javascript 复制代码
false

则表示同步请求。

浏览器会被阻塞。

现代开发已经基本废弃同步请求。


二十、send() 发送请求

代码:

javascript 复制代码
xhr.send();

作用:

text 复制代码
正式发送HTTP请求

很多初学者认为:

javascript 复制代码
xhr.send();

执行后程序会停下来等待。

实际上不会。


例如:

javascript 复制代码
console.log("A");

xhr.send();

console.log("B");

输出:

text 复制代码
A
B

说明:

text 复制代码
请求发送

≠

等待响应

这正是 Ajax 的异步特性。


二十一、请求状态监听

代码:

javascript 复制代码
xhr.onreadystatechange = function(){

}

21.1 什么是 onreadystatechange

每当请求状态发生变化。

浏览器都会触发:

javascript 复制代码
onreadystatechange

回调函数。


因此:

javascript 复制代码
console.log(xhr.readyState);

会输出多个数字。

因为请求过程经历了多个阶段。


二十二、readyState 状态详解

XMLHttpRequest 一共有五种状态。


0:UNSENT

text 复制代码
对象创建完成

例如:

javascript 复制代码
const xhr =
new XMLHttpRequest();

1:OPENED

text 复制代码
已经调用open()

例如:

javascript 复制代码
xhr.open(...)

2:HEADERS_RECEIVED

text 复制代码
已经收到响应头

服务器已经响应。

但数据还未接收完成。


3:LOADING

text 复制代码
正在接收数据

数据不断从服务器传输过来。


4:DONE

text 复制代码
请求完成

数据接收结束。

可以开始处理结果。


因此:

javascript 复制代码
console.log(xhr.readyState);

通常会看到:

text 复制代码
1
2
3
4

或者:

text 复制代码
2
3
3
4

具体取决于网络环境。


二十三、HTTP 状态码

代码:

javascript 复制代码
xhr.status === 200

表示:

text 复制代码
HTTP响应状态码

常见状态码:

200

text 复制代码
OK

请求成功


404

text 复制代码
Not Found

资源不存在


500

text 复制代码
Internal Server Error

服务器内部错误


因此:

javascript 复制代码
xhr.status === 200 &&
xhr.readyState === 4

表示:

text 复制代码
请求成功完成

此时可以安全处理数据。


二十四、解析服务器返回的数据

代码:

javascript 复制代码
const todos =
JSON.parse(
    xhr.responseText
);

24.1 responseText 是什么

服务器返回:

json 复制代码
[
    {
        "id":1,
        "title":"过四六级"
    }
]

很多人误以为:

javascript 复制代码
responseText

得到的是数组。

实际上不是。


得到的是:

text 复制代码
JSON字符串

例如:

javascript 复制代码
'[{"id":1,"title":"过四六级"}]'

本质仍然是字符串。


24.2 JSON.parse

因此需要:

javascript 复制代码
JSON.parse()

进行反序列化。

转换过程:

text 复制代码
JSON字符串

↓

JSON.parse()

↓

JavaScript对象

这与服务端的:

javascript 复制代码
JSON.stringify()

形成对应关系。


服务端:

text 复制代码
对象

↓

JSON.stringify()

↓

字符串

浏览器:

text 复制代码
字符串

↓

JSON.parse()

↓

对象

二十五、动态渲染页面

获取数据后:

javascript 复制代码
todos.map(
    todo =>
    `<li>${todo.title}</li>`
)

假设数据:

javascript 复制代码
[
    {
        title:"过四六级"
    },
    {
        title:"回家过节"
    }
]

生成:

javascript 复制代码
[
    "<li>过四六级</li>",
    "<li>回家过节</li>"
]

随后:

javascript 复制代码
.join("")

转换为:

html 复制代码
<li>过四六级</li><li>回家过节</li>

最终:

javascript 复制代码
document
.getElementById("todos")
.innerHTML = ...

更新页面。

浏览器渲染结果:

text 复制代码
• 过四六级

• 回家过节

二十六、完整请求流程分析

到这里,我们已经完成了一个完整的 Ajax 通信案例。

但是对于初学者来说,最容易混乱的地方在于:

text 复制代码
到底是谁先执行?
谁在等待?
数据又是怎么回来的?

下面我们从用户打开页面开始,梳理整个请求流程。


26.1 页面加载

浏览器加载:

html 复制代码
index.html

开始执行 JavaScript:

javascript 复制代码
console.log("start");

输出:

text 复制代码
start

接着注册按钮点击事件:

javascript 复制代码
document
.getElementById("btn")
.addEventListener(...)

此时:

text 复制代码
事件注册完成

↓

等待用户点击

但并不会立即执行回调函数。


26.2 创建 XMLHttpRequest 对象

执行:

javascript 复制代码
const xhr = new XMLHttpRequest();

浏览器创建一个 HTTP 请求对象。

此时:

text 复制代码
readyState = 0

26.3 配置请求

执行:

javascript 复制代码
xhr.open(
    "GET",
    "http://localhost:3000/todos",
    true
);

请求配置完成。

状态变为:

text 复制代码
readyState = 1

26.4 注册状态监听

执行:

javascript 复制代码
xhr.onreadystatechange = function(){

}

注册回调函数。

此时浏览器开始监听请求状态变化。


26.5 发送请求

执行:

javascript 复制代码
xhr.send();

浏览器正式向服务器发送请求。

注意:

text 复制代码
发送请求

≠

等待响应

请求发送后。

JavaScript 会继续向下执行:

javascript 复制代码
console.log("end");

输出:

text 复制代码
end

26.6 Node服务器接收请求

服务器收到:

text 复制代码
GET /todos

请求。

进入:

javascript 复制代码
if(req.url === "/todos")

对应逻辑。


然后返回:

javascript 复制代码
JSON.stringify(todos)

生成的 JSON 字符串。


26.7 浏览器收到响应

收到响应后:

javascript 复制代码
onreadystatechange

被触发。

状态依次变化:

text 复制代码
2

↓

3

↓

4

当:

javascript 复制代码
xhr.readyState === 4

并且:

javascript 复制代码
xhr.status === 200

说明请求成功完成。


26.8 解析 JSON 数据

执行:

javascript 复制代码
JSON.parse(
    xhr.responseText
)

完成反序列化。

转换过程:

text 复制代码
JSON字符串

↓

JavaScript对象

26.9 更新页面

执行:

javascript 复制代码
innerHTML = ...

更新 DOM。

浏览器重新渲染页面。

最终显示:

text 复制代码
过四六级

回家过节

二十七、为什么 Ajax 能实现局部刷新

很多初学者会产生一个疑问:

text 复制代码
为什么 Ajax 请求不会刷新整个页面?

原因在于:

Ajax 请求获取的是:

text 复制代码
数据

而不是:

text 复制代码
新的HTML页面

传统网页:

text 复制代码
请求

↓

返回HTML

↓

重新渲染整个页面

Ajax:

text 复制代码
请求

↓

返回JSON

↓

JavaScript处理数据

↓

更新部分DOM

因此:

text 复制代码
页面无需重新加载

这就是局部刷新的本质。


二十八、Fetch:现代 Ajax 方案

虽然 XMLHttpRequest 仍然能够工作。

但现代开发更推荐:

javascript 复制代码
fetch()

例如:

javascript 复制代码
fetch(
    "http://localhost:3000/todos"
)
.then(res => res.json())
.then(data => {
    console.log(data);
});

代码更加简洁。

逻辑也更加清晰。


Fetch 与 XMLHttpRequest 对比

XMLHttpRequest

javascript 复制代码
const xhr = new XMLHttpRequest();

xhr.open(...);

xhr.onreadystatechange = function(){

};

xhr.send();

Fetch

javascript 复制代码
fetch(url)
.then(...)
.then(...)

明显更加简单。


二十九、Async 与 Await

现代项目中最推荐的写法是:

javascript 复制代码
async/await

例如:

javascript 复制代码
async function getTodos(){

    const res =
        await fetch(
            "http://localhost:3000/todos"
        );

    const data =
        await res.json();

    console.log(data);

}

相比 Promise:

javascript 复制代码
fetch(...)
.then(...)
.then(...)
.then(...)

Async/Await 具有以下优势:

  • 更接近同步代码
  • 可读性更高
  • 更容易维护
  • 错误处理更统一

因此已经成为当前前端开发主流方案。


总结

本文通过一个完整的 TodoList 案例,系统学习了 Ajax 的核心知识。

主要内容包括:

  • Ajax 的本质与作用
  • JavaScript 单线程模型
  • Event Loop 事件循环
  • JSON.stringify 与 JSON.parse
  • Node.js HTTP 服务
  • 路由机制
  • CORS 跨域问题
  • XMLHttpRequest 工作原理
  • readyState 状态变化
  • HTTP 状态码
  • DOM 动态渲染
  • Fetch 与 Async/Await

通过这个案例,我们实际上已经完成了一次完整的前后端通信实践:

text 复制代码
浏览器

↓

Ajax请求

↓

Node服务器

↓

JSON数据

↓

浏览器解析

↓

DOM渲染

↓

页面更新

理解这一套流程之后,再学习 Express、数据库、RESTful API、Vue、React 等技术时,就能够更加清晰地理解它们背后的运行原理。

相关推荐
SwJieJie2 小时前
Webpack vs Vite 构建工程化实战(Vue 项目深度解析)
前端·vue.js·webpack·node.js
伶俜663 小时前
鸿蒙原生应用实战(九)ArkUI 天气预报 App:HTTP 请求 + 定位 + 动效
http·华为·harmonyos
l1o3v1e4ding3 小时前
windows安装Claude Code,并接入Deepseek-v4模型 ,提供离线安装包
git·npm·node.js·claude code·cc-switchcc
逻极3 小时前
HTTP/HTTPS 协议从入门到精通:从原理到性能提升400%的完整路径(协议优化实战)
网络协议·http·性能优化·https·tls
芒鸽3 小时前
HarmonyOS 网络编程实战:HTTP、WebSocket 与 Socket 通信详解
网络·http·harmonyos
努力的lpp3 小时前
渗透主流工具完整参数手册(sqlmap、Nmap、Hydra、Dirsearch、Xray)
javascript·网络协议·测试工具·安全·http·工具
李白的天不白4 小时前
http https
网络协议·http·https
Rain50916 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
拾年27518 小时前
从零手写 Ajax:用原生 XHR 搭建前后端交互全流程
前端·javascript·ajax