网站
引入
JS 代码默认是一行一行执行的,前一行没执行完,后一行就得等着。
但有些操作比较耗时,比如请求网络数据、读取文件、等待用户点击。如果这些操作也让代码干等着,整个页面就会卡住。
为了解决这个问题,JS 引入了异步编程的概念:耗时操作可以在后台执行,不阻塞后续代码。
正文
同步
定义
同步是指代码按照书写顺序,一行一行地执行,每一行都必须等上一行执行完才能开始。
console.log("第一步");
console.log("第二步");
console.log("第三步");输出结果:
第一步
第二步
第三步
三行代码依次执行,每行都等上一行完成后才开始。
再看一个例子:
let a = 1;
let b = 2;
let c = a + b;
console.log(c);c 的值必须等 a + b 算完之后才能得到。这就是同步。
同步的特点:简单直观,代码从上往下按顺序走,容易理解和调试。
异步
定义
异步是指某些操作不会阻塞后续代码的执行,而是在后台运行,完成后再回来通知结果。
console.log("开始");
setTimeout(function () {
console.log("定时器执行");
}, 2000);
console.log("结束");输出结果:
开始
结束
定时器执行
执行流程:
- 打印
"开始" - 设置一个 2 秒后执行的定时器,不会在这里等待
- 继续往下走,打印
"结束" - 2 秒后,定时器回调函数执行,打印
"定时器执行"
异步的特点:耗时操作不会卡住后面的代码。
同步和异步的对比
| 对比 | 同步 | 异步 |
|---|---|---|
| 执行顺序 | 严格按顺序 | 不阻塞后续代码 |
| 是否等待 | 等上一行完成 | 后台执行,完成后回调 |
| 代码理解 | 简单直观 | 需要注意执行顺序 |
| 适用场景 | 简单计算 | 网络请求、定时器、用户事件 |
回调函数
定义
回调函数是作为参数传给另一个函数的函数,在特定时机被调用。
function doTask(callback) {
console.log("正在执行任务...");
callback();
}
doTask(function () {
console.log("任务完成");
});输出:
正在执行任务...
任务完成
doTask() 接收一个函数作为参数,任务做完后调用这个函数。这个被传入的函数就是回调函数。
回调函数的常见场景
定时器回调:
setTimeout(function () {
console.log("2 秒后执行");
}, 2000);事件回调:
const btn = document.querySelector("#btn");
btn.addEventListener("click", function () {
console.log("按钮被点击了");
});网络请求回调:
fetch("/api/data")
.then(function (response) {
return response.json();
})
.then(function (data) {
console.log(data);
});常见的异步操作
| 异步操作 | 说明 |
|---|---|
setTimeout() | 延迟执行 |
setInterval() | 定时重复执行 |
| 网络请求 | fetch()、XMLHttpRequest |
| 文件读取 | FileReader |
| 用户事件 | click、input、keydown |
| 图片加载 | img.onload |
回调地狱
当多个异步操作需要按顺序执行时,回调函数会一层套一层,代码变得很难读。
setTimeout(function () {
console.log("第一步完成");
setTimeout(function () {
console.log("第二步完成");
setTimeout(function () {
console.log("第三步完成");
setTimeout(function () {
console.log("第四步完成");
}, 1000);
}, 1000);
}, 1000);
}, 1000);这段代码的问题:
- 缩进越来越深,像金字塔
- 很难看清执行顺序
- 修改和维护都很困难
- 错误处理很麻烦
这就是回调地狱(Callback Hell)。
模拟一个回调地狱场景
function getUser(callback) {
setTimeout(function () {
callback({ name: "Tom" });
}, 1000);
}
function getOrders(user, callback) {
setTimeout(function () {
callback(["订单 1", "订单 2"]);
}, 1000);
}
function getOrderDetail(order, callback) {
setTimeout(function () {
callback({ name: order, price: 99 });
}, 1000);
}调用时:
getUser(function (user) {
console.log("用户:" + user.name);
getOrders(user, function (orders) {
console.log("订单:" + orders);
getOrderDetail(orders[0], function (detail) {
console.log("订单详情:" + detail.name + ",价格:" + detail.price);
});
});
});每一步都依赖上一步的结果,回调嵌套越来越深。
解决回调地狱的思路
为了解决回调地狱的问题,JS 后来引入了更好的方案:
- Promise:用链式调用代替嵌套回调
- async/await:让异步代码看起来像同步代码
getUser()
.then(function (user) {
return getOrders(user);
})
.then(function (orders) {
return getOrderDetail(orders[0]);
})
.then(function (detail) {
console.log(detail);
});用 Promise 改写后,代码从左到右平铺,不再层层嵌套。
常见写法
| 写法 | 作用 |
|---|---|
setTimeout(fn, time) | 延迟执行回调 |
element.addEventListener(event, fn) | 事件回调 |
fetch(url).then(fn) | 网络请求回调 |
function task(cb) { cb(); } | 自定义回调 |
特点
- 同步代码按顺序执行,容易理解
- 异步代码不会阻塞主线程
- 回调函数是最基础的异步处理方式
- 回调嵌套过多会导致回调地狱
- Promise 和 async/await 是更好的异步方案
注意事项
- 异步代码的执行顺序和书写顺序可能不同
- 不要在异步回调外面直接使用异步结果
- 回调地狱不是语法错误,是代码结构问题
- 回调函数本身是同步传入的,只是执行时机由异步操作决定
let data = null;
setTimeout(function () {
data = "异步结果";
}, 1000);
console.log(data);这里 console.log(data) 会输出 null,因为定时器还没执行,data 还没被赋值。
正确做法是在回调里使用数据:
setTimeout(function () {
data = "异步结果";
console.log(data);
}, 1000);理解
可以把同步理解为排队买奶茶,每个人必须等前面的人买完才能轮到自己。
异步就是先下单拿个号,去做别的事情,做好了叫号来取。
回调函数就是那个”叫号”的动作,任务做完了,通过回调通知你结果。
回调地狱就是连续排了好几次队,每次都要等上一次排完,越等越烦。
引出
为了解决回调地狱的问题,ES6 引入了 JS Promise,用链式调用来替代层层嵌套。
理解 Promise 之后,还可以继续学习 JS async await,它能让异步代码写起来更像同步代码。
JS 定时器 是最常见的异步操作之一,可以结合定时器加深对异步执行顺序的理解。