2378 字
12 分钟
JavaScript 异步编程完整教程(setTimeout、Promise、async/await)

JavaScript 异步编程完整教程(setTimeout、Promise、async/await)#

上一篇学完 ES6+,你已经能看懂大部分现代 JS 代码了。但有一类代码你可能还是觉得”怪”——比如 setTimeout 里的回调、then 链、async/await。这些都属于异步编程,是 JavaScript 里最重要也最容易绕晕的概念。

这一篇我会用一个生活比喻贯穿始终,保证你听完不再懵。


一、同步 vs 异步:用”餐厅点餐”理解#

同步(Synchronous):排队办理#

想象你去银行柜台办业务:只有一个窗口,你必须站着等前面的人办完,才轮到你。你干等着,什么都做不了。

代码里的同步就是:一行代码执行完,才执行下一行。如果某行代码很慢(比如要等 3 秒),后面的代码就全部卡住。

console.log("第一行");
console.log("第二行");
console.log("第三行");
// 输出顺序:第一行 → 第二行 → 第三行(按顺序,没毛病)

异步(Asynchronous):扫码点餐#

想象你到餐厅吃饭:服务员收走你的菜单,告诉你”稍等,菜好了叫你”,然后你就自由了,可以刷手机、聊天。菜做好了,服务员再端给你。

代码里的异步就是:先不等着慢任务完成,继续往下执行其他代码。等慢任务完成时,再回来处理结果。

console.log("开始点餐");
setTimeout(() => {
console.log("菜做好了!");
}, 3000);
console.log("继续刷手机");
// 输出顺序:开始点餐 → 继续刷手机 → (3秒后)菜做好了!

注意!setTimeout 不会让程序等 3 秒,而是”3 秒后执行里面的函数”。程序先打印”继续刷手机”,3 秒后才打印”菜做好了”。


二、单线程:JS 为什么需要异步?#

JavaScript 是单线程语言——同一时间只能干一件事。就像只有一个厨师,一次只能做一道菜。

如果所有事情都同步处理,遇到慢操作(比如请求网络数据要 2 秒),页面就”卡死”了:按钮点不动、动画停了、页面白屏。这体验太差了。

所以 JS 设计了事件循环(Event Loop)机制:

  1. 主线程先执行同步代码
  2. 遇到异步任务(定时器、网络请求),把它交给浏览器”后台处理”,主线程继续干别的
  3. 异步任务完成后,把回调函数放到一个”待办队列”里
  4. 主线程空闲时,从队列里取出回调执行

你不用记住全部细节,只要理解:异步 = 不阻塞主线程,回头再处理


三、定时器:setTimeout 和 setInterval#

setTimeout:等一会儿执行一次#

// 3 秒后执行一次
setTimeout(() => {
console.log("3 秒到了");
}, 3000);

单位是毫秒(1 秒 = 1000 毫秒)。

传参数#

function greet(name) {
console.log(`你好,${name}`);
}
setTimeout(greet, 2000, "小明"); // 2秒后:你好,小明

清除定时器:clearTimeout#

let timer = setTimeout(() => {
console.log("这条不会打印");
}, 5000);
clearTimeout(timer); // 取消定时器

setInterval:每隔一段时间重复执行#

let count = 0;
let interval = setInterval(() => {
count++;
console.log(`第 ${count} 次`);
if (count >= 3) {
clearInterval(interval); // 到 3 次停止
}
}, 1000);
// 输出:第 1 次 → 第 2 次 → 第 3 次(每秒一次)

四、回调函数:异步的”老办法”#

回调(callback)就是把一个函数作为参数,传给另一个函数,让它”办完事后调用你”。

function 买菜(蔬菜, 完成) {
console.log(`去买${蔬菜}`);
setTimeout(() => {
console.log(`${蔬菜}买回来了`);
完成(); // 办完了,调用回调
}, 2000);
}
买菜("白菜", () => {
console.log("开始做饭");
});
// 2秒后:白菜买回来了 → 开始做饭

回调地狱:多层嵌套的噩梦#

如果要做的事有先后顺序:买菜 → 洗菜 → 切菜 → 炒菜,用回调写就会一层套一层:

买菜("白菜", () => {
洗菜(() => {
切菜(() => {
炒菜(() => {
console.log("开饭!");
});
});
});
});

这还只是 4 层,实际项目可能十几层。代码向右歪成三角形,读起来想哭——这就是著名的回调地狱(Callback Hell)

解决办法: Promise。这正是下一篇讲 Promise 的原因。


五、Promise:把异步”包装”成可读的流程#

Promise 是什么?#

Promise 是一个”承诺”对象,代表一个还没完成、但将来会完成的操作。它有三种状态:

  1. pending(进行中):操作还没完成
  2. fulfilled(成功):操作完成,有结果
  3. rejected(失败):操作失败,有错误原因

状态只能从 pending 变成 fulfilled 或 rejected,一旦改变就定死了。

创建一个 Promise#

let promise = new Promise((resolve, reject) => {
// 这里放异步任务
setTimeout(() => {
let success = true; // 模拟成功/失败
if (success) {
resolve("数据加载成功"); // 成功 → 调用 resolve 传结果
} else {
reject("出错了"); // 失败 → 调用 reject 传错误
}
}, 2000);
});

resolvereject 是两个函数,由 JS 提供,你不用自己实现。

使用 Promise:then / catch#

promise
.then(result => {
console.log("成功:", result); // 成功:数据加载成功
})
.catch(error => {
console.log("失败:", error);
});
  • .then(函数):成功时执行,参数是 resolve 传的值
  • .catch(函数):失败时执行,参数是 reject 传的值

读起来像人话: “先做这个,成功就怎样,失败就怎样”。

链式调用:解决回调地狱#

then 可以链式写,每个 then 的返回值会传给下一个:

function 买菜(蔬菜) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`${蔬菜}买回来了`);
resolve(蔬菜);
}, 1000);
});
}
买菜("白菜")
.then( => {
console.log("洗" + 菜);
return 菜; // 传给下一个 then
})
.then( => {
console.log("切" + 菜);
return 菜;
})
.then( => {
console.log("炒" + 菜);
console.log("开饭!");
});
// 依次输出:白菜买回来了 → 洗白菜 → 切白菜 → 炒白菜 → 开饭!

对比之前的回调地狱,现在代码是从上往下一条直线,清楚多了。

finally:无论成败都执行#

promise
.then(result => console.log("成功"))
.catch(error => console.log("失败"))
.finally(() => console.log("收尾工作,比如关闭加载动画"));

Promise.all:多个异步一起等#

let p1 = Promise.resolve("第一个结果");
let p2 = new Promise(resolve => setTimeout(() => resolve("第二个结果"), 2000));
Promise.all([p1, p2]).then(results => {
console.log(results); // ["第一个结果", "第二个结果"](等全部完成)
});

Promise.all所有 Promise 都成功才执行 then,任何一个失败就进 catch。适合”同时请求多个数据”的场景。


六、async/await:让异步代码”看起来像同步”#

Promise 已经比回调好了,但 .then() 链还是不够直观。ES2017 推出的 async/await,让你用写同步代码的感觉来写异步代码——这现在是主流写法。

基本用法#

// 1. 函数前面加 async
async function getData() {
// 2. 在 Promise 前面加 await,等它完成
let result = await 一个Promise;
return result;
}

规则只有两条:

  1. async 放在函数前,函数就变成了”异步函数”
  2. 在异步函数里,await 后面跟一个 Promise,代码会停下来等这个 Promise 完成,然后拿到结果

完整示例#

function 买菜(蔬菜) {
return new Promise((resolve) => {
setTimeout(() => resolve(`${蔬菜}买回来了`), 1000);
});
}
async function 做饭流程() {
let a = await 买菜("白菜"); // 等1秒,拿到结果
console.log(a);
let b = await 买菜("猪肉"); // 再等1秒
console.log(b);
console.log("开饭!");
}
做饭流程();
// 白菜买回来了 → 猪肉买回来了 → 开饭!

注意:虽然 await 让这段代码看起来是”一步一步走”,但它不会卡住页面——其他代码照样可以运行。

错误处理:try / catch#

async/await 里处理错误,用和同步代码一样的 try/catch:

function 请求数据() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("网络中断");
}, 1000);
});
}
async function main() {
try {
let data = await 请求数据();
console.log(data);
} catch (error) {
console.log("出错了:", error); // 出错了:网络中断
}
}
main();

await 的 Promise 如果失败(reject),就会抛出一个错误,被 catch 接住。这样错误处理清清楚楚。

async 函数总是返回 Promise#

async function foo() {
return 42; // 返回普通值
}
foo().then(value => console.log(value)); // 42

即使 async 函数里返回普通值,外部拿到的也是一个 Promise,需要用 then 或 await 取。


七、模拟一个真实的异步场景#

把学到的串起来:模拟”请求用户数据 → 处理后显示”:

// 模拟网络请求:1.5 秒后返回数据
function 请求用户(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "小明", score: 95 });
} else {
reject("用户不存在");
}
}, 1500);
});
}
async function 展示用户(id) {
console.log("加载中...");
try {
let user = await 请求用户(id);
console.log(`用户:${user.name},成绩:${user.score}`);
} catch (error) {
console.log("加载失败:", error);
}
}
展示用户(1); // 加载中... → 用户:小明,成绩:95
展示用户(-1); // 加载中... → 加载失败:用户不存在

“加载中”先出现(异步不阻塞),1.5 秒后结果出现。这就是真实项目中”先显示 loading,再显示数据”的原理。


八、总结#

这一篇的核心要点:

  • 同步:一行接一行执行,慢操作会卡住页面
  • 异步:先干别的,慢任务完成后再回来处理(餐厅点餐比喻)
  • 单线程 + 事件循环:JS 不阻塞主线程的底层机制
  • setTimeout / setInterval:定时器,异步入门
  • 回调函数:老办法,嵌套多了就是回调地狱
  • Promise:三种状态(pending/fulfilled/rejected),then/catch/finally 链式处理
  • async/await:用同步的写法写异步,try/catch 处理错误,现在是主流

下一篇我们学 fetch 网络请求,把 async/await 用在真正的”请求后端数据”上——那时你会发现,前面学的全都在真实项目里用上了。

JavaScript 异步编程完整教程(setTimeout、Promise、async/await)
https://021028.xyz/posts/default/68/
作者
021028
发布于
2026-08-11
许可协议
CC BY-NC-SA 4.0