JavaScript异步编程通俗指南:从混乱回调到优雅代码

JSON 2026-01-27 15:43:12 255

一、先搞懂:为什么非要用异步?

JavaScript有个天生的特点:单线程。就像食堂只有一个打饭窗口,所有任务都得排队来,前一个做完才能轮到下一个。如果所有操作都“同步”执行(排队等),遇到网络请求、读取文件、大数据计算这种耗时操作时,程序就会“卡死”——页面动不了、按钮点不动,用户体验直接拉满。异步编程就是为了解决这个问题:让耗时操作“去后台排队”,不耽误主线程做其他事(比如渲染页面、响应用户点击),等耗时操作完成后,再回头处理结果。举个生活化例子:

  • 同步:买奶茶必须等前一个人拿到手,你才能点单(全程阻塞);
  • 异步:点单后拿个取号器,去旁边刷手机,等叫号再回来取(不阻塞,高效)。

二、异步编程的演进之路:从混乱到优雅

异步的核心需求一直没变,但写法在不断升级,每一代都在解决上一代的“痛点”。我们按时间顺序,从最简单的写法开始讲。

1. 回调函数:异步的“原始方式”

这是最基础、最原生的异步写法,核心逻辑是:把异步操作的处理逻辑,封装成一个函数(回调函数),等异步任务完成后,自动调用这个函数。

常见场景&例子

定时器、DOM点击事件、早期网络请求,都用这种方式:

// 1. 定时器回调(最常见)
setTimeout(() => {
  console.log("1秒后执行(异步)");
}, 1000);
console.log("先执行我(主线程)"); // 输出顺序:先执行我 → 1秒后执行

// 2. Node.js里的文件读取(错误优先回调)
const fs = require('fs');
// 第一个参数固定是错误对象,成功则为null
fs.readFile('./test.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读文件失败:', err);
    return;
  }
  console.log('文件内容:', data);
});

最大痛点:回调地狱

如果需要多个异步操作“串行执行”(比如先请求用户信息,再用用户ID请求订单,再用订单ID请求商品),代码会嵌套成“金字塔”,越写越乱,这就是“回调地狱”。

// 回调地狱示例:三层嵌套,可读性极差
requestData('/api/user', (user) => {
  console.log('拿到用户信息');
  requestData(`/api/orders?userId=${user.id}`, (orders) => {
    console.log('拿到订单信息');
    requestData(`/api/goods?orderId=${orders[0].id}`, (goods) => {
      console.log('拿到商品信息');
    }, (err) => console.error('商品请求失败'));
  }, (err) => console.error('订单请求失败'));
}, (err) => console.error('用户请求失败'));

  问题总结:嵌套深、错误处理分散、逻辑绕,后期根本没法维护。

2. 事件/发布-订阅:解耦的异步方式

为了解决回调函数“耦合紧”的问题,出现了“事件监听”模式。核心思路是:异步任务完成后,只“喊一声”(触发事件),谁关心这个结果,谁就提前“等着”(监听事件)。就像公众号推送:作者(发布者)只负责发文章,粉丝(订阅者)关注后就能收到,作者不用管谁在看。

常见场景&例子

DOM事件、组件通信(如Vue EventBus)都用这种模式:

// 1. 简单DOM事件(点击事件:点击后触发回调)
document.getElementById('btn').addEventListener('click', () => {
  console.log('按钮被点击了(异步响应)');
});

// 2. 自定义发布-订阅(简化版Vue EventBus)
class EventBus {
  constructor() {
    this.listeners = {}; // 存事件和对应的回调
  }
  // 订阅事件(等着接收通知)
  on(eventName, callback) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName].push(callback);
  }
  // 发布事件(发送通知)
  emit(eventName, data) {
    this.listeners[eventName]?.forEach(callback => callback(data));
  }
}

// 使用
const bus = new EventBus();
// 订阅“数据准备好”事件
bus.on('dataReady', (data) => console.log('收到数据:', data));
// 模拟异步任务,完成后发布事件
setTimeout(() => {
  bus.emit('dataReady', { id: 1, content: '异步数据' });
}, 1000);

优缺点

优点:解耦(发布者和订阅者互不依赖)、支持多个回调同时响应;缺点:事件触发顺序难追踪、没法直接获取异步结果,复杂场景容易乱。

3. 传统Ajax:异步网络请求的初代写法

早期前端请求后端数据,靠的是Ajax(核心是XMLHttpRequest对象),本质就是“回调函数+网络请求”,也是回调地狱的重灾区。

简单例子(原生Ajax)

// 原生Ajax请求函数
function requestData(url, successCb, errorCb) {
  const xhr = new XMLHttpRequest(); // 创建请求对象
  xhr.open('GET', url, true); // 配置请求(方法、地址、异步)
  xhr.onload = function() {
    // 请求完成且成功
    if (xhr.status >= 200 && xhr.status < 300) {
      successCb(JSON.parse(xhr.responseText));
    } else {
      errorCb(new Error(`请求失败:${xhr.status}`));
    }
  };
  xhr.onerror = function() {
    errorCb(new Error('网络出问题了'));
  };
  xhr.send(); // 发送请求
}

// 调用(还是依赖回调)
requestData(
  'https://jsonplaceholder.typicode.com/todos/1',
  (data) => console.log('成功:', data),
  (err) => console.error('失败:', err)
);

核心问题

和回调函数一样,串行请求时会陷入回调地狱,且每次请求都要写重复代码(创建对象、配置参数),复用性差。

4. Promise(ES6):告别回调地狱的第一步

ES6引入的Promise,是异步编程的“里程碑”。它把异步操作的“结果”和“处理逻辑”分开,用“链式调用”替代嵌套,彻底解决回调地狱。可以把Promise理解成“快递单”:下单后(创建Promise),快递有三种状态,你不用一直等,后续只需要关注“签收”(成功)或“丢件”(失败)的处理。

核心概念(超通俗版)

  • pending(运输中):异步任务还在执行;
  • fulfilled(已签收):异步任务成功,拿到结果;
  • rejected(已丢件):异步任务失败,拿到错误原因;
  • then():签收成功后的处理;
  • catch():丢件后的统一处理;
  • finally():不管成功失败,最后都要做的事(比如关闭加载动画)。

用Promise封装Ajax(摆脱回调)

// Promise版Ajax请求
function requestData(url) {
  // 返回一个Promise对象(快递单)
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.onload = function() {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText)); // 成功:标记为“已签收”,传递结果
      } else {
        reject(new Error(`请求失败:${xhr.status}`)); // 失败:标记为“已丢件”,传递错误
      }
    };
    xhr.onerror = function() {
      reject(new Error('网络错误'));
    };
    xhr.send();
  });
}

链式调用:解决回调地狱

Promise的then()方法会返回新的Promise,支持链式写法,代码从“纵向嵌套”变成“横向平铺”:

// 串行请求:链式调用替代嵌套
requestData('https://jsonplaceholder.typicode.com/users/1')
  .then((user) => {
    console.log('拿到用户信息:', user);
    // 继续返回新的Promise(下一张快递单)
    return requestData(`/users/${user.id}/todos`);
  })
  .then((todos) => {
    console.log('拿到待办列表:', todos);
    return requestData(`/todos/${todos[0].id}`);
  })
  .then((detail) => console.log('拿到待办详情:', detail))
  .catch((err) => console.error('任意一步出错:', err)) // 统一处理所有错误
  .finally(() => console.log('请求结束(不管成功失败)'));

批量处理异步任务

Promise提供了三个实用方法,应对多异步任务场景:

方法
作用
适用场景
Promise.all([p1,p2])
等所有任务都成功,才返回结果;一个失败则全失败
同时请求多个独立接口(如加载用户+商品数据)
Promise.race([p1,p2])
谁先完成(成功/失败),就返回谁的结果
请求超时控制(如5秒没响应就提示超时)
Promise.allSettled([p1,p2])
等所有任务都完成,不管成败,返回每个任务状态
批量上传文件(需知道每个文件的上传结果)

5. async/await(ES7):异步编程的“终极优雅写法”

async/await是ES7引入的“语法糖”,底层还是基于Promise,但能让异步代码看起来和同步代码一模一样,彻底告别链式调用的思维负担。

核心语法(两步搞定)

  • async:给函数加个“异步标记”,函数返回值会自动包装成Promise;
  • await:只能在async函数里用,意思是“等一等”——等后面的Promise完成,再继续执行下面的代码。

简单例子

// async函数:自动返回Promise
async function demo() {
  return 100; // 等同于Promise.resolve(100)
}
demo().then(res => console.log(res)); // 输出100

// await等待Promise结果
async function fetchData() {
  // 等请求完成,再把结果赋值给user(同步写法,异步执行)
  const user = await requestData('https://jsonplaceholder.typicode.com/users/1');
  console.log('拿到用户信息:', user);
  return user;
}

改写串行请求:极致简洁

async function fetchAllData() { try { // 第一步:请求用户(等完成再走下一步) const user = await requestData('https://jsonplaceholder.typicode.com/users/1'); console.log('用户信息:', user); // 第二步:请求待办(依赖用户ID) const todos = await requestData(`/users/${user.id}/todos`); console.log('待办列表:', todos); // 第三步:请求待办详情(依赖待办ID) const detail = await requestData(`/todos/${todos[0].id}`); console.log('待办详情:', detail); return detail; } catch (err) { // 统一捕获所有错误(任何await失败都会进这里) console.error('请求出错:', err); } finally { console.log('请求流程结束'); } } // 调用 fetchAllData();

异步并行请求(提升性能)

await默认是“串行等待”,如果多个请求互不依赖,用Promise.all+await实现并行,节省时间:

async function fetchParallel() { try { // 同时发起三个请求(并行执行) const userPromise = requestData('https://jsonplaceholder.typicode.com/users/1'); const todoPromise = requestData('https://jsonplaceholder.typicode.com/todos/1'); const postPromise = requestData('https://jsonplaceholder.typicode.com/posts/1'); // 等所有请求完成,一次性拿结果 const [user, todo, post] = await Promise.all([userPromise, todoPromise, postPromise]); console.log('并行请求结果:', { user, todo, post }); } catch (err) { console.error('有请求失败:', err); } }

6. 补充方案:应对复杂场景

除了主流写法,还有两种方案针对特殊场景,作为补充:

RxJS:复杂异步流的“瑞士军刀”

适合处理“多源异步、高频交互”场景(如实时搜索、WebSocket数据),能轻松实现防抖、节流、取消请求等功能。缺点是学习曲线陡,简单场景没必要用。

Web Workers:解决主线程阻塞

如果有大数据计算、复杂逻辑处理(如图片压缩),用Web Workers开个“子线程”专门处理,不耽误主线程渲染页面。缺点是子线程不能操作DOM,只能和主线程传数据。

三、总结:不同场景选哪种方案?

按“简单到复杂”梳理,直接对号入座:

场景
推荐方案
简单异步(单次回调、定时器、DOM事件)
回调函数 / 事件监听
组件通信、多回调响应
发布-订阅(EventBus)
普通网络请求(串行/并行)
async/await + Promise(首选)
实时数据、高频交互、复杂流处理
RxJS
大数据计算、耗时逻辑(避免页面卡死)
Web Workers

最后:异步编程的核心,始终是“在单线程里,不阻塞主线程地处理耗时操作”。async/await是当前最优雅的写法,但理解它底层的Promise、回调函数,才能应对所有异步场景,真正做到“知其然也知其所以然”。

版权所属:SO JSON在线解析

原文地址:https://www.sojson.com/blog/572.html

转载时必须以链接形式注明原始出处及本声明。

本文主题:

如果本文对你有帮助,那么请你赞助我,让我更有激情的写下去,帮助更多的人。

关于作者
一个低调而闷骚的男人。
相关文章
JS加密,JS在线淆加密,JavaScript在线加密
关于本站所有JavaScript 加密、淆、解密、美化等安全说明
Javascript模块化编程,本站使用 require.js 优化JS加载, require.js下载
JS解密,JavaScript 解密,任何JS加密都能100%解密,以及JS 加密讲解
常用的js淆加密方式
Javascript 生成UUID,Java生成UUID
javascript核心知识点
Jackson 美化输出JSON,优雅的输出JSON数据,格式化输出JSON数据... ...
javascript核心知识点
分解质因数 JavaScript 计算方式,及分解质因数讲解
最新文章
文件上传漏洞与防御 4058
前端构建工具选型指南:Webpack、Vite、Rollup、esbuild 深度对比 1444
物联网时代2026年时序数据库选型指南 1151
SaaS行业面临AI挑战:从“无限复用”到“灵活适应” 1269
神经网络:从构造到模型训练全链路解析 1168
一文吃透 Redis 核心存储结构:ziplist、listpack 与哈希表扩容 / 并发查询 1593
Linux sudo提权完整指南:从基础用法到生产级安全配置 691
XSS 和 CSRF 的本质区别及开发防御全解析 772
JVM垃圾回收(GC)全维度解析:从原理到调优实战 813
Linux动静态库与ELF加载全解析:从实操制作到底层原理 912
最热文章
免费天气API,天气JSON API,不限次数获取十五天的天气预报 783114
最新MyEclipse8.5注册码,有效期到2020年 (已经更新) 711464
苹果电脑Mac怎么恢复出厂系统?苹果系统怎么重装系统? 679993
Jackson 时间格式化,时间注解 @JsonFormat 用法、时差问题说明 562673
我为什么要选择RabbitMQ ,RabbitMQ简介,各种MQ选型对比 512621
Elasticsearch教程(四) elasticsearch head 插件安装和使用 484794
Jackson 美化输出JSON,优雅的输出JSON数据,格式化输出JSON数据... ... 302947
Java 信任所有SSL证书,HTTPS请求抛错,忽略证书请求完美解决 247433
Elasticsearch教程(一),全程直播(小白级别) 233097
谈谈斐讯路由器劫持,你用斐讯路由器,你需要知道的事情 228329
支付扫码

所有赞助/开支都讲公开明细,用于网站维护:赞助名单查看

查看我的收藏

正在加载... ...