😭从回调地狱到 async/await:一文打通 Ajax 与 JS 异步编程

一、先搞懂:JS 为什么需要异步?

JavaScript 是单线程语言 ------ 这意味着它同一时间只能干一件事。

但现实是残酷的:网络请求、定时器、用户点击...... 这些任务都很 "慢",如果 JS 死等它们完成,页面就直接卡死了。

怎么办?答案就是 Event Loop(事件循环)

遇到异步任务,JS 先把它 "寄存" 到事件队列里,继续往下执行同步代码;等时机到了(比如请求回来了、用户点了按钮),再把回调函数拿出来执行。

这就是为什么你在代码里会看到 console.log('start')console.log('end') 先打印,而请求结果后出来 ------同步先走,异步后到

二、异步编程的 "三代进化"

第一代:回调函数(Callback)

最原始的方式 ------ 把 "干完之后要做的事" 写成一个函数,传进去等着被调用。

js

ini 复制代码
xhr.onreadystatechange = function() {
    if (xhr.status === 200 && xhr.readyState === 4) {
        const todos = JSON.parse(xhr.responseText);
        // 拿到数据后渲染页面
    }
}

缺点:嵌套多层就会形成 "回调地狱",代码横向发展,可读性和维护性极差。

第二代:Promise + then ()

ES6 带来了 Promise,把异步任务封装成一个 "承诺" 对象,用 .then() 链式调用。

js

ini 复制代码
fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => console.log(data))

优点:链式调用,代码纵向发展,逻辑更清晰。

第三代:async /await(最推荐)

ES7 的终极方案 ------ 让异步代码看起来跟同步代码一模一样

js

javascript 复制代码
async function fetchTodos() {
    const res = await fetch('http://localhost:3000/todos');
    const data = await res.json();
    console.log(data);
}

为什么最香?

  • 写法最直观,没有回调嵌套,也不用写 .then()
  • 错误处理可以直接用 try/catch,跟同步代码一致
  • 调试方便,断点能像同步代码一样一步步走

三、Ajax:前端动态刷新的基石

什么是 Ajax?

A synchronous J avaScript A nd XML------ 异步 JavaScript 和 XML。

简单说就是:JS 主动发起 HTTP 请求,拿到数据后局部更新页面,整个过程页面不刷新

这可是 Web 2.0 时代的核心技术,没有它就没有今天的各种动态网页。

两种实现方式

1. XMLHttpRequest(老前辈)

js

javascript 复制代码
const xhr = new XMLHttpRequest();          // 1. 创建对象
xhr.open('GET', 'http://localhost:3000/todos', true); // 2. 打开通道

xhr.onreadystatechange = function() {      // 3. 监听状态变化
    if (xhr.status === 200 && xhr.readyState === 4) {
        const todos = JSON.parse(xhr.responseText); // 4. 解析响应
        // 5. 渲染到页面
        document.getElementById('todos').innerHTML = 
            todos.map(todo => `<li>${todo.title}</li>`).join('');
    }
}

xhr.send(); // 6. 发送请求

readyState 有 5 个状态值(0~4),等于 4 才代表请求完全完成。写起来比较繁琐,所以有了更现代的 fetch。

2. Fetch API(现代推荐)

js

ini 复制代码
fetch('http://localhost:3000/todos')
    .then(res => res.json())
    .then(data => {
        document.getElementById('todos').innerHTML = 
            data.map(todo => `<li>${todo.title}</li>`).join('');
    });

基于 Promise 设计,配合 async/await 更香。

四、JSON.stringify:对象与字符串的 "翻译官"

网络传输只能传字符串 ,但 JS 里我们用的是对象 ------ 这就需要一个翻译官:JSON.stringify

js

javascript 复制代码
JSON.stringify(value, replacer?, space?)
  • value:要序列化的对象
  • replacer :过滤器,传 null 就原样序列化
  • space:缩进空格数,团队规范里一般设 2 或 4,提升可读性

后端返回数据时:

js

javascript 复制代码
res.end(JSON.stringify(todos)); // 对象 → JSON 字符串

前端拿到后再 "翻译" 回来:

js

ini 复制代码
const todos = JSON.parse(xhr.responseText); // JSON 字符串 → 对象

一去一回,数据就在前后端之间流动起来了。

五、前后端联动:一个完整的 Todo 列表示例

后端(Node.js 原生 http 模块)

js

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

http.createServer((req, res) => {
    const todos = [
        { id: "1", title: "过四六级", completed: false },
        { id: "2", title: "回家过节", completed: false }
    ];
    
    if (req.url === '/todos') {
        res.setHeader('Access-Control-Allow-Origin', '*'); // 跨域放行
        res.setHeader('Content-Type', 'application/json; charset=utf-8');
        res.end(JSON.stringify(todos)); // 序列化后返回
    }
}).listen(3000, () => {
    console.log('server is running on 3000 port');
});

几个关键点:

  • CommonJS 模块化 :Node 早期用 require + module.exports,后来升级为 ESM 的 import/export
  • 跨域头Access-Control-Allow-Origin: * 允许任意域名访问,开发环境常用
  • Content-Type:告诉浏览器返回的是 JSON 数据,别当纯文本处理

前端(HTML + 原生 JS)

html

预览

xml 复制代码
<ul id="todos"></ul>
<button id="btn">按钮</button>

<script>
console.log('start');

// 事件监听也是异步------用户不点,回调永远不执行
document.getElementById('btn')
    .addEventListener('click', () => {
        console.log('点击按钮');
    });

const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000/todos', true);

xhr.onreadystatechange = function() {
    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('');
    }
}

xhr.send();
console.log('end');
</script>

打印顺序一定是:

plaintext

sql 复制代码
start
end
(然后等请求回来,渲染列表)

这就是异步的魔力 ------ 不等请求,代码先跑完。

六、一张图总结:异步进化路线

plaintext

javascript 复制代码
回调函数(callback)
    ↓ 嵌套地狱
Promise + then()
    ↓ 链式调用
async / await  ← 目前最优解

记住一句话:能 async/await 就别 then,能 then 就别写回调嵌套。代码是写给人看的,顺便给机器运行 ------ 可读性永远第一位。

相关推荐
weedsfly2 小时前
语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌
前端·javascript
JustHappy2 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js
晓得迷路了3 小时前
栗子前端技术周刊第 134 期 - React Router v8、TypeScript 7 RC、React Native 0.86...
前端·javascript·react.js
代码煮茶18 小时前
React 组件封装方法论 —— 以 Todo App 为例
javascript·react.js
任沫18 小时前
Agent之Function Call
javascript·人工智能·go
默_笙20 小时前
🛬 我让 AI 帮我写了一个打飞机游戏,结果 Canvas 把我整不会了
前端·javascript
梯度不陡20 小时前
AI 到底能不能从零写软件?ProgramBench 和 RepoZero 给出了两种答案
前端·javascript·面试
胡萝卜术21 小时前
滑动窗口最大值:从暴力到单调队列,层层优化全解析
前端·javascript·面试
kyriewen1 天前
2026 年了,这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代
前端·javascript·npm