异步编程与网络请求

JavaScript 是单线程的:一个主线程执行所有代码。如果网络请求是”同步”的,发请求时页面会整个卡死直到响应返回。所以 JS 用异步机制处理耗时操作(网络请求、定时器、文件读取)。

这一篇讲异步编程的核心(回调 → Promise → async/await)和网络请求(fetch / AJAX),是 Web API 阶段最核心、也最容易拉开差距的一篇。

一、同步 vs 异步

1
2
3
4
5
6
7
8
9
10
// 同步:顺序执行,遇到耗时操作就阻塞
console.log("1");
const result = 同步请求(); // 假设这里要等 3 秒,页面卡死
console.log("2");

// 异步:不阻塞,先执行后面的代码
console.log("1");
fetch(url).then(() => console.log("2")); // 请求完成后才执行
console.log("3");
// 输出顺序:1 → 3 → 2

事件循环(简要)

  • JS 有执行栈(同步代码)和任务队列(异步回调)。
  • 同步代码先全部执行完,异步回调进入任务队列,等主线程空闲后依次执行。
  • 宏任务:setTimeout、setInterval、I/O、事件;微任务:Promise.then、async/await。
  • 每轮事件循环:先执行完微任务,再执行下一个宏任务。

记忆点:同步代码先跑完,异步回调后执行。这就是为什么 setTimeout(fn, 0) 也不是”立刻”执行。

二、回调函数与回调地狱

早期异步靠回调函数实现,但层层嵌套导致”回调地狱”:

1
2
3
4
5
6
7
getData(function (a) {
getData2(a, function (b) {
getData3(b, function (c) {
console.log(c); // 嵌套越来越深
});
});
});

问题:可读性差、难维护、错误处理困难。于是有了 Promise。

三、Promise

1. 三种状态

  • pending(进行中)→ fulfilled(成功)/ rejected(失败)
  • 状态一旦改变就不可逆,只能从 pending 变一次。

2. 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("成功"); // 成功时调用
// reject("失败"); // 失败时调用
}, 1000);
});

p.then(result => {
console.log(result); // "成功"
}).catch(err => {
console.log(err); // "失败"
}).finally(() => {
console.log("无论成功失败都执行");
});

3. 链式调用

then 返回一个新 Promise,可以一直链下去:

1
2
3
4
5
6
fetch("/user")
.then(res => res.json()) // 返回新 Promise
.then(user => fetch(`/user/${user.id}`))
.then(res => res.json())
.then(detail => console.log(detail))
.catch(err => console.log("出错", err)); // 一个 catch 兜底整条链

4. Promise.all / race

1
2
3
4
5
6
7
Promise.all([p1, p2, p3]).then(results => {
console.log(results); // 全部成功才执行,返回结果数组
}).catch(err => {}); // 任一失败就走 catch

Promise.race([p1, p2]).then(result => {
console.log(result); // 谁先完成用谁的结果
});

Java 对照:Promise ≈ CompletableFuture(thenApply/thenCompose 对应 then,allOf/anyOf 对应 all/race)。

四、async / await

async/await 是 Promise 的语法糖,让异步代码像同步一样写。

1
2
3
4
5
6
7
async function getData() {
const res = await fetch("/user"); // await 等待 Promise 结果
const data = await res.json();
return data; // async 函数总是返回 Promise
}

getData().then(data => console.log(data));

错误处理用 try/catch:

1
2
3
4
5
6
7
8
9
async function getData() {
try {
const res = await fetch("/user");
const data = await res.json();
console.log(data);
} catch (err) {
console.log("请求失败", err);
}
}

⚠️ 易错点:async 函数必须 await 才能拿到值,直接调用返回的是 Promise 对象。await 只能用在 async 函数内部。

五、网络请求 fetch

fetch 是现代的 Promise 风格请求 API。

1. GET 请求(默认)

1
2
3
fetch("https://api.example.com/users")
.then(res => res.json()) // 解析响应为 JSON
.then(data => console.log(data));

2. POST 请求

1
2
3
4
5
6
7
8
9
fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "Tom", age: 18 }) // 请求体
})
.then(res => res.json())
.then(data => console.log(data));

3. 用 async/await 改写

1
2
3
4
5
6
async function getUsers() {
const res = await fetch("https://api.example.com/users");
if (!res.ok) throw new Error("请求失败 " + res.status);
const data = await res.json();
return data;
}

4. 响应对象常用属性/方法

属性/方法 说明
res.json() 解析为 JSON 对象(返回 Promise)
res.text() 解析为文本
res.status 状态码(200、404、500…)
res.ok 是否成功(状态码 200~299)

⚠️ fetch 最大的坑:HTTP 错误状态(404、500)不会自动 reject,fetch 只会在网络故障时 reject。所以必须手动检查 res.ok:

1
2
3
4
5
6
7
fetch(url)
.then(res => {
if (!res.ok) throw new Error("请求失败 " + res.status);
return res.json();
})
.then(data => console.log(data))
.catch(err => console.log(err));

六、XMLHttpRequest(经典 AJAX,简要)

XMLHttpRequest(XHR)是 AJAX 的经典实现,回调风格,写法繁琐,现已被 fetch 逐步取代:

1
2
3
4
5
6
7
8
9
10
11
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/users");
xhr.onload = function () {
if (xhr.status === 200) {
console.log(JSON.parse(xhr.responseText));
}
};
xhr.onerror = function () {
console.log("请求失败");
};
xhr.send();

对照:fetch 是 Promise 风格(简洁、可链式),XHR 是回调风格(繁琐)。现代项目基本用 fetch 或 axios。

七、跨域 CORS

1. 同源策略

浏览器安全机制:默认只允许同源(协议 + 域名 + 端口三者完全相同)的请求访问数据。

  • 同源:https://a.com/page 请求 https://a.com/api ✅
  • 跨域:https://a.com 请求 https://b.com/api ❌(协议/域名/端口任一不同)

2. 解决跨域的常见思路

方案 说明
CORS 后端设置响应头 Access-Control-Allow-Origin,主流方案
代理 开发时用 devServer 代理,生产用 Nginx 反向代理
JSONP 利用 <script> 标签跨域,只支持 GET,已很少用
1
2
// 后端 CORS 响应头(示意)
// Access-Control-Allow-Origin: *

记忆点:跨域是浏览器的限制(服务器之间互相请求没有跨域问题),所以”代理”方案能绕过——请求先到同源服务器,再由服务器转发。

八、速查表

需求 写法
创建 Promise new Promise((resolve, reject) => {})
成功/失败回调 p.then(onOk).catch(onErr)
全部完成 Promise.all([p1, p2])
竞速 Promise.race([p1, p2])
定义 async 函数 async function f() {}
等待结果 const x = await promise
异步错误捕获 try { await ... } catch (err) {}
GET 请求 fetch(url).then(res => res.json())
POST 请求 fetch(url, { method: "POST", body: JSON.stringify(...) })
检查是否成功 if (!res.ok) throw new Error(...)

九、复习要点

  1. JS 是单线程,耗时操作靠异步;同步代码先执行完,异步回调后执行(事件循环)。
  2. 回调地狱 → Promise → async/await 是异步的三代演进。
  3. Promise 三状态(pending/fulfilled/rejected),状态不可逆;then 返回新 Promise 可链式。
  4. Promise.all 全部成功才执行,race 谁先完成用谁。
  5. async 函数返回 Promise,必须 await 才能拿值;await 只能在 async 函数内。
  6. fetch 返回 Promise,用 res.json() 解析;**HTTP 错误不会自动 reject,要检查 res.ok**。
  7. POST 要设 method、headers、body(对象要 JSON.stringify)。
  8. 跨域是同源策略(浏览器限制),解决靠 CORS / 代理 / JSONP。