JavaScript 异步编程
为什么需要异步
JavaScript 是单线程语言,一次只能做一件事。如果同步执行网络请求、文件读取、定时器等"耗时操作",后面的代码只能干等,页面会卡死、按钮点了没反应。异步编程让这些耗时任务"先挂起,结果回来再继续",主线程始终保持流畅。
同步与异步的执行顺序
看一个直观的例子:
console.log("开始"); // 输出:开始
setTimeout(function () {
console.log("定时任务");
}, 0);
console.log("结束"); // 输出:结束
// 整体输出顺序:开始 → 结束 → 定时任务
即使延时设为 0,回调也要等当前同步代码全部跑完才执行,这就是异步"先挂起、后回来"的特点。
回调函数:最基础的异步写法
把"事情做完后要干什么"写成一个函数传进去,等结果好了再调用它:
function loadData(callback) {
setTimeout(function () {
var data = "服务端数据";
callback(data); // 模拟数据到达后回调
}, 1000);
}
loadData(function (msg) {
console.log("收到:" + msg); // 1 秒后输出:收到:服务端数据
});
console.log("这句先执行"); // 输出:这句先执行
事件驱动异步
浏览器大量使用事件驱动异步:点击、键盘、加载完成等,回调登记后由事件触发:
<button id="btn">点我</button>
<script>
document.getElementById("btn").onclick = function () {
console.log("按钮被点击"); // 每次点击时才输出
};
window.onload = function () {
console.log("页面加载完成");
};
</script>
效果:页面加载完输出"页面加载完成",之后每次点击按钮输出"按钮被点击"。
回调地狱:嵌套过深的问题
异步任务一环套一环时,回调层层嵌套,代码又丑又难维护:
getUser(function (user) {
getOrder(user.id, function (order) {
getDetail(order.id, function (detail) {
console.log(detail); // 三层缩进,真实项目可能更深
});
});
});
这种"向右生长"的代码就是著名的回调地狱(callback hell)。
更现代的解决方案
ES6 起,JavaScript 提供了更优雅的异步工具:
- Promise:把异步结果包装成对象,用 then/catch 链式处理(下一节详解)。
- async/await:让异步代码写起来像同步代码一样直观。 请先记住"回调 → Promise → async/await"这条演进路线,后续章节会依次展开。
小结
异步编程让单线程的 JavaScript 不被耗时任务阻塞。最基础的写法是回调函数,但嵌套过多会形成回调地狱;后续的 Promise 与 async/await 正是为解决它而生。