一、先搞懂: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 就别写回调嵌套。代码是写给人看的,顺便给机器运行 ------ 可读性永远第一位。