前端三基石(四)JavaScript 异步编程与网络请求

目录

  • 前言
  • 一、为什么需要异步?
  • 二、回调函数
  • 三、Promise
    • [3.1 什么是 Promise?](#3.1 什么是 Promise?)
    • [3.2 Promise 的状态](#3.2 Promise 的状态)
    • [3.3 创建 Promise](#3.3 创建 Promise)
    • [3.4 链式调用](#3.4 链式调用)
    • [3.5 错误处理](#3.5 错误处理)
    • [3.6 静态方法](#3.6 静态方法)
  • 四、async/await
    • [4.1 基本语法](#4.1 基本语法)
    • [4.2 错误处理](#4.2 错误处理)
    • [4.3 并行执行](#4.3 并行执行)
  • [五、Fetch API](#五、Fetch API)
    • [5.1 基本用法](#5.1 基本用法)
    • [5.2 处理响应](#5.2 处理响应)
    • [5.3 发送数据](#5.3 发送数据)
    • [5.4 错误处理](#5.4 错误处理)
  • 六、跨域问题
    • [6.1 什么是跨域?](#6.1 什么是跨域?)
    • [6.2 解决方案](#6.2 解决方案)
  • 七、实战示例
  • 八、总结

前言

在浏览器中,网络请求、定时器、文件读取等操作都是"耗时的"。如果同步等待,页面会卡死。

JavaScript 采用异步编程模型:发起请求后立即继续执行,等结果就绪再处理回调。


一、为什么需要异步?

同步 vs 异步

js 复制代码
// 同步:按顺序执行,前一步完成才执行下一步
console.log('开始');
const result = heavyTask();  // 假设耗时 3 秒
console.log('结果:', result);
console.log('结束');

// 执行顺序:开始 → (等待 3 秒) → 结果 → 结束
js 复制代码
// 异步:发起后立即继续,不等待结果
console.log('开始');
setTimeout(() => {
  console.log('3秒后');
}, 3000);
console.log('结束');

// 执行顺序:开始 → 结束 → (3秒后) → 3秒后

为什么 JS 需要异步?

  • JS 是单线程的,同一时间只能做一件事
  • 网络请求可能很慢,同步等待会阻塞页面
  • 异步让 UI 保持响应,用户体验更好

二、回调函数

传统异步编程使用回调函数:

js 复制代码
// 定时器
setTimeout(function() {
  console.log('1秒后');
}, 1000);

// 事件绑定
btn.addEventListener('click', function() {
  console.log('点击了');
});

问题:回调地狱(Callback Hell)

当多个异步操作有依赖关系时:

js 复制代码
// 回调地狱示例
getUserInfo(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      getComments(detail.id, function(comments) {
        console.log(comments);
      });
    });
  });
});

层层嵌套,难以阅读和维护。


三、Promise

3.1 什么是 Promise?

Promise 是 ES6 引入的异步编程解决方案,用链式调用替代回调嵌套:

js 复制代码
// Promise 版本
getUserInfo(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetail(orders[0].id))
  .then(detail => getComments(detail.id))
  .then(comments => console.log(comments))
  .catch(err => console.error(err));

3.2 Promise 的状态

Promise 有三种状态:

状态 含义 特点
pending 进行中 初始状态
fulfilled 已成功 不可变
rejected 已失败 不可变

状态只能改变一次:pendingfulfilledpendingrejected

3.3 创建 Promise

js 复制代码
// 创建 Promise
const promise = new Promise(function(resolve, reject) {
  // 异步操作
  setTimeout(function() {
    const success = true;

    if (success) {
      resolve('成功的结果');  // 状态变为 fulfilled
    } else {
      reject(new Error('失败的原因'));  // 状态变为 rejected
    }
  }, 1000);
});

// 使用 Promise
promise
  .then(result => {
    console.log('成功:', result);
  })
  .catch(error => {
    console.log('失败:', error);
  });

后端类比 :Promise 类似于 Java 的 CompletableFuture,表示一个未来会完成的操作。

3.4 链式调用

then 返回一个新的 Promise,可以继续链式调用:

js 复制代码
fetch('/api/user')
  .then(response => response.json())  // 返回 Promise
  .then(user => {
    console.log(user);
    return fetch(`/api/orders?userId=${user.id}`);  // 返回 Promise
  })
  .then(response => response.json())
  .then(orders => console.log(orders));

3.5 错误处理

js 复制代码
// 方式一:catch 捕获链中所有错误
fetch('/api/user')
  .then(response => response.json())
  .then(user => {
    if (!user) {
      throw new Error('用户不存在');
    }
    return user;
  })
  .catch(error => {
    console.error('出错了:', error);
  });

// 方式二:then 的第二个参数
promise.then(
  result => console.log(result),
  error => console.error(error)
);

推荐使用 catch:代码更清晰,捕获所有错误。

3.6 静态方法

js 复制代码
// Promise.resolve():直接返回成功状态的 Promise
Promise.resolve(42).then(val => console.log(val));  // 42

// Promise.reject():直接返回失败状态的 Promise
Promise.reject(new Error('失败')).catch(err => console.log(err));

// Promise.all():所有都成功才成功,一个失败就失败
Promise.all([
  fetch('/api/user'),
  fetch('/api/orders')
])
  .then(([userRes, ordersRes]) => {
    // 两个请求都成功
  })
  .catch(err => {
    // 任一请求失败
  });

// Promise.allSettled():等待所有完成(无论成功失败)
Promise.allSettled([
  fetch('/api/user'),
  fetch('/api/orders')
])
  .then(results => {
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        console.log('成功:', result.value);
      } else {
        console.log('失败:', result.reason);
      }
    });
  });

// Promise.race():返回最先完成的(无论成功失败)
Promise.race([
  fetch('/api/slow'),
  fetch('/api/fast')
])
  .then(response => {
    // 最先返回的请求
  });

// Promise.any():返回第一个成功的,全部失败才失败
Promise.any([
  fetch('/api/backup1'),
  fetch('/api/backup2')
])
  .then(response => {
    // 第一个成功的请求
  })
  .catch(errors => {
    // 全部失败
  });
方法 作用
all() 全部成功才成功
allSettled() 等待全部完成
race() 返回最快完成的
any() 返回第一个成功的

四、async/await

4.1 基本语法

async/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码:

js 复制代码
// async 函数返回 Promise
async function getUser() {
  return '张三';  // 相当于 return Promise.resolve('张三')
}

getUser().then(name => console.log(name));

// await 等待 Promise 完成
async function fetchUser() {
  const response = await fetch('/api/user');  // 等待请求完成
  const user = await response.json();          // 等待解析完成
  console.log(user);
  return user;
}

fetchUser();

对比 Promise 链式调用

js 复制代码
// Promise 方式
function fetchUser() {
  return fetch('/api/user')
    .then(response => response.json())
    .then(user => {
      console.log(user);
      return user;
    });
}

// async/await 方式(更直观)
async function fetchUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  console.log(user);
  return user;
}

4.2 错误处理

使用 try...catch 处理错误:

js 复制代码
async function fetchUser() {
  try {
    const response = await fetch('/api/user');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const user = await response.json();
    return user;
  } catch (error) {
    console.error('请求失败:', error);
    throw error;  // 可以选择继续抛出
  }
}

4.3 并行执行

await 是顺序执行的,多个独立请求会串行:

js 复制代码
// 串行执行(慢)
async function fetchAll() {
  const user = await fetch('/api/user').then(r => r.json());
  const orders = await fetch('/api/orders').then(r => r.json());
  return { user, orders };
}

要并行执行,用 Promise.all

js 复制代码
// 并行执行(快)
async function fetchAll() {
  const [user, orders] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/orders').then(r => r.json())
  ]);
  return { user, orders };
}

五、Fetch API

Fetch 是浏览器提供的现代网络请求 API,基于 Promise。

5.1 基本用法

js 复制代码
// GET 请求(默认)
fetch('/api/user')
  .then(response => response.json())
  .then(user => console.log(user));

// async/await 方式
async function getUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  console.log(user);
}

5.2 处理响应

js 复制代码
const response = await fetch('/api/user');

// 检查响应状态
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

// 不同格式解析
const json = await response.json();     // JSON
const text = await response.text();     // 文本
const blob = await response.blob();     // 二进制(文件)
const arrayBuffer = await response.arrayBuffer();  // ArrayBuffer

// 获取响应头
console.log(response.headers.get('Content-Type'));
console.log(response.status);  // 状态码
console.log(response.ok);      // 是否成功(200-299)

5.3 发送数据

js 复制代码
// POST 请求
async function createUser(userData) {
  const response = await fetch('/api/user', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(userData)
  });

  return response.json();
}

// 使用示例
createUser({ name: '张三', age: 25 })
  .then(user => console.log('创建成功:', user));

// 发送 FormData(文件上传)
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);

  const response = await fetch('/api/upload', {
    method: 'POST',
    body: formData  // 浏览器自动设置 Content-Type
  });

  return response.json();
}

完整配置选项

js 复制代码
fetch('/api/user', {
  method: 'POST',        // 请求方法
  headers: {             // 请求头
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token'
  },
  body: JSON.stringify(data),  // 请求体
  mode: 'cors',          // 跨域模式
  credentials: 'include', // 是否携带 Cookie
  cache: 'no-cache',     // 缓存策略
  signal: controller.signal  // 用于取消请求
});

5.4 错误处理

Fetch 只在网络错误时 reject,HTTP 错误状态(如 404、500)不会触发 catch:

js 复制代码
fetch('/api/user/999')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  })
  .catch(error => {
    // 只捕获网络错误
    // 不捕获 HTTP 错误(需要在上面手动抛出)
  });

推荐封装

js 复制代码
async function request(url, options = {}) {
  const response = await fetch(url, options);

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`HTTP ${response.status}: ${error}`);
  }

  return response.json();
}

// 使用
try {
  const user = await request('/api/user');
  console.log(user);
} catch (error) {
  console.error('请求失败:', error);
}

六、跨域问题

6.1 什么是跨域?

浏览器的同源策略限制:协议 + 域名 + 端口必须完全相同。

复制代码
当前页面:https://example.com

允许访问:
  ✅ https://example.com/api/user(同源)

禁止访问:
  ❌ http://example.com/api/user(协议不同)
  ❌ https://api.example.com/user(域名不同)
  ❌ https://example.com:8080/api/user(端口不同)

6.2 解决方案

方案一:CORS(Cross-Origin Resource Sharing)

服务端设置响应头:

复制代码
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Spring Boot 示例:

java 复制代码
@CrossOrigin(origins = "https://example.com")
@RestController
public class UserController {
    // ...
}

方案二:代理服务器

前端请求同源代理服务器,代理服务器请求目标服务器:

复制代码
浏览器 → 代理服务器(同源) → 目标服务器

开发环境常用 webpack 或 vite 配置代理:

js 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://target-server.com',
        changeOrigin: true
      }
    }
  }
}

方案三:JSONP(已过时)

利用 <script> 标签不受同源限制的特性。只支持 GET 请求,不推荐使用。


七、实战示例

示例:用户登录并获取信息

js 复制代码
async function login(username, password) {
  try {
    // 登录
    const loginRes = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, password })
    });

    if (!loginRes.ok) {
      const msg = await loginRes.text();
      throw new Error(`登录失败:${msg}`);
    }

    const { token } = await loginRes.json();

    // 获取用户信息
    const userRes = await fetch('/api/user/info', {
      headers: { 'Authorization': `Bearer ${token}` }
    });

    if (!userRes.ok) {
      throw new Error('获取用户信息失败');
    }

    const user = await userRes.json();
    console.log('登录成功:', user);
    return user;

  } catch (error) {
    console.error(error);
    throw error;
  }
}

// 使用
login('admin', '123456').catch(err => {
  alert('登录失败,请重试');
});

八、总结

类别 内容 要点
异步概念 为什么需要异步 单线程、不阻塞 UI
回调函数 传统方式 回调地狱问题
Promise 异步编程核心 三种状态、链式调用、静态方法
async/await 语法糖 同步写法、try-catch 处理错误
Fetch API 网络请求 基于 Promise、注意 HTTP 错误处理
跨域 同源策略 CORS、代理

核心记忆点

  1. Promise :链式调用解决回调地狱,all 并行执行
  2. async/await :让异步代码像同步一样,用 try...catch 处理错误
  3. Fetchresponse.ok 判断成功,HTTP 错误不会触发 catch
  4. 跨域:开发用代理,生产用 CORS

本文是"前端三基石"系列第二篇的续篇。结合前文的 DOM 操作,你已经掌握了 JavaScript 在浏览器中的核心能力:操作页面、响应用户、与服务器通信。

相关推荐
星夜夏空995 小时前
网络编程(1)
服务器·网络
上海云盾-小余5 小时前
中小站点防护避坑:低价高防服务存在的各类安全短板剖析
网络·爬虫·安全·ddos
瑞瑞小同学5 小时前
处理echarts x轴内容过多,重叠问题
前端·javascript·echarts
HexCIer5 小时前
面向未来的原子化 CSS:UnoCSS 核心架构分析与 Tailwind CSS 现状
前端·css·vite
程序员黑豆6 小时前
鸿蒙应用开发中的单位详解:px、vp、fp、lpx
前端·harmonyos
像我这样帅的人丶你还6 小时前
MCP + npm:给五年前的老系统接上AI
前端·javascript·agent
南一Nanyi6 小时前
依赖注入和控制反转
前端·设计模式·nestjs
小村儿6 小时前
连载14-实战篇--一个半月,我一个人和 Claude Code 搭出一套数字人工程
前端·后端·ai编程
愚公移码6 小时前
蓝凌EKP18产品:流程虚拟机(PVM)
java·开发语言·前端
promiseThen7 小时前
5 分钟上手 Markdown:标题到表格、代码块与简历实战
前端