家人们,我又来了!好久不见,啊啊啊啊啊啊啊啊啊啊啊啊啊,今天来分享异步这个知识点!
一. 引言:为什么需要异步编程?
在编程中,同步(Synchronous) 和异步(Asynchronous) 是两种不同的代码执行方式,直接影响程序的性能、响应速度和开发模式。
同步(Synchronous)
:代码按顺序执行,当前任务未完成时,后续任务必须等待。简单直观,但可能阻塞主线程,资源利用率低,适合简单场景。异步(Asynchronous)
:任务提交后立即继续执行后续逻辑,不等待结果,通过回调、事件或线程池处理结果。可提高资源利用率和程序响应性,适合高并发、I/O 密集型或耗时操作,但代码复杂性较高,需注意线程安全等问题。
而在JS中,代码的运行过程是单线程的
,那什么是线程呢?为什么是单线程呢?
线程和进程是计算机操作系统中非常重要的概念,它们分别代表了不同的程序执行单元和资源管理方式。
进程
:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。线程
:是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。
举个例子来说,就像我们在点开一个浏览器的页面窗口时,就相当于单开了一个进程,而在页面打开时页面的渲染、数据请求等等就相当于是一个个线程。
特别注意的是
:JavaScript (JS) 执行和页面渲染在浏览器中是互斥的,不能同时进行。因为 JavaScript 可以修改 DOM 和 CSSOM,从而影响页面的布局和样式,所以当 JavaScript 引擎执行脚本时,它会阻止浏览器进行页面渲染。同样,当浏览器正在进行页面渲染时,JavaScript 的执行会被暂停,直到渲染完成。
因此,JS 引擎在执行 JavaScript 代码时,只有一个线程在运行(除了人为开辟新线程)。如果没有异步处理,那js的执行效率就非常低,任务阻塞会非常严重。所以JS在遇到需要耗时执行的代码就将其先挂起,等到后续不耗时的代码执行完毕后,再回过头来执行耗时的代码,这也就是我们说的异步。
那异步的方案有哪些呢?哪种异步方案更优?
二. Callback:最基础的异步方案
- 原理:函数作为参数传递,完成后触发回调。
- 代码示例:
JavaScript代码
function a(){
setTimeout(() => {
console.log('a 执行完毕')
},1000)
}
function b(){
setTimeout(() => {
console.log('b 执行完毕')
},1500)
}
function c(){
setTimeout(() => {
console.log('c 执行完毕')
},5000)
}
function d(){
console.log('d 执行完毕')
}
a()
b()
c()
d()
如上面代码所示,因为a、b、c三个函数的内部都是耗时代码,如果不使用回调函数,即不在setTimeout
内部代码执行完再执行下一个函数的话,就会导致没有耗时代码的函数d直接执行,1秒后输出"a 执行完毕",1.5秒后输出"b 执行完毕",5秒后输出"c 执行完毕"如图所示: 所以我们想要a、b、c、d的打印按顺序执行,就得使用回调,让每个函数内部的耗时代码执行完后触发回调,下一个函数才执行,代码如下:
JavaScript代码
function a(cb,cb2,cb3){
setTimeout(() => {
console.log('a 执行完毕')
cb(cb2,cb3) //callback b(c, d)
},1000)
}
function b(cb,cb3){
setTimeout(() => {
console.log('b 执行完毕')
cb(cb3) //callback c(d)
},1500)
}
function c(cb){
setTimeout(() => {
console.log('c 执行完毕')
cb() //callback d()
},5000)
}
function d(){
console.log('d 执行完毕')
}
a(b,c,d)
结果如下: 可以看出回调函数来解决异步问题是非常好理解,也很容易上手的。但是,上面代码示例只是简单的四个函数进行回调嵌套,都看起来有点麻烦。而在实际的开发过程中,可能会出现几十甚至几百个函数相互之间要使用回调,那将会非常麻烦,出现错误的时候也会很难找。总结用回调解决异步的
优缺点
如下:
-
优点:简单、兼容性高(所有JS环境支持)
-
缺点 :会出现回调地狱(Callback Hell),代码嵌套过深,错误处理困难(需手动检查
err
),代码难以维护。
三. Promise:拯救回调的"承诺"
1. 什么是 Promise?
Promise 是 JavaScript 中处理异步操作的对象,代表一个未来才会完成的操作 (如网络请求、文件读取等等)。它的原理是通过 状态机(pending/resolved/rejected)+ 链式调用来完成。
2. Promise 的三种状态
-
Pending(进行中) :初始状态,操作未完成。
-
Fulfilled(已成功) :操作成功完成,返回结果值,即
resolve()
回调函数成功执行,状态转变为Fulfilled,可以在后续操作中获取返回的结果。 -
Rejected(已失败) :操作失败,返回错误原因,有两种情况:
- 同步代码中抛出错误(
throw
)
如果在 Promise 的构造函数或
then/catch
回调 中同步抛出错误 (throw
),Promise 会自动进入Rejected
状态,并在后续的.catch()
中可以捕获该错误。相当于隐式调用了reject(error)
。
2. 异步代码中抛出错误如果在 异步回调 (如
setTimeout
、fetch.then
)中throw
,Promise 无法自动捕获 ,会导致全局未处理的错误,需手动 调用reject
。 - 同步代码中抛出错误(
值得注意的是:Promise的状态 不可逆
(一旦从 Pending 变为 Fulfilled/Rejected,就不能再改变)。
3. 代码示例:
JavaScript代码
function a(){
return new Promise(function(resolve,reject){
// throw new Error("同步错误!"); // 自动触发 reject(error)
setTimeout(function(){
console.log('a');
// resolve('a 执行完毕') // resolve 调用结束了,.then() 里面的回调函数才会触发
// reject('a 失败了') // 人为报错
},1000)
})
}
function b(){
console.log('b');
}
a()
// 箭头函数的参数只有一个,小括号也可以省略
.then(res => { //a()的结果是一个 Promise实例对象,所以 .then() 方法是在Promise构造函数的原型上
console.log(res);
b()
})
.catch((err) => {
// console.log("捕获到:", err);
console.log(err); // 捕获错误
})
如上代码示例,如果resolve不调用
,则Promise的状态还是pending
,即后续.then()
或者.catch()
操作无法继续执行,结果如下图:
如果resolve调用
,则Promise的状态转换为resolved()
,即后续.then()
操作执行,结果如下图:
如果在Promise的构造函数
的同步代码中抛出错误(throw
),就相当于隐式调用了reject
,可以直接在catch中获取错误,如下图:
异步代码中,如果reject()
调用,则Promise的状态转换为rejected()
,即后续.catch()
操作执行,结果如下图:
4. then()的返回值
对于then方法,其实也有一套规则决定其返回值的状态:
- then 方法一定会返回一个新的 Promise对象,状态为pending;
- then 方法的pending状态会根据上一个 Promise 状态的修改而修改;
- then 方法可以用自己的
return
覆盖掉then默认返回的promise对象。
这里我们用一段代码举例,来解释then()
方法的返回值:
JavaScript代码
let count = 0
function A() {
return new Promise((resolve, reject) => {
setTimeout(() => {
count = 1000
// resolve() // pending状态 改为 resolved状态
}, 1000)
})
}
function B() {
return new Promise((resolve, reject) => {
setTimeout(() => {
count = 2000
resolve()
}, 500);
})
}
function C() {
console.log(count);
}
A().then()
.then(() => {
C()
})
// A().then(() => {
// B().then(() => {
// C()
// })
// })
// 优化后
// A().then(() => {
// return B()
// })
// .then(() => {
// C()
// })
首先,第24行代码then的后面还能够接then,说明then天生返回一个promise对象,才能接.then。
其次,我们让A返回的Promise状态还是pending
,直接运行代码,会发现A运行后不再执行后续then,结果如下: 接着,如果我们将A的Promise对象状态改为
resolved
,第二个then也执行了,所以能够执行从c()的调用。这就说明第一个then返回的Promise状态由pending改为了resolved 了,才能继续走第二个then。如图: 如果我想要函数A调用后,执行函数B之后再执行函数C,就会想到在then里面调用B,然后再接then,如代码29-33行,这样如果函数多了起来,就会显得很麻烦。
于是,我们就会用到第三个规则,return重新返回一个Promise对象,如代码35-41行。利用then后面还可以接then的方式,我们在第一个then的最后返回一个状态为resolved
的promise对象B,这样接在后面的then可以直接执行,一下就清晰了代码的运行顺序,更加便捷不易出错。
5. Promise的优缺点
-
优点:
- 链式调用解决回调嵌套
- 统一的错误处理(
.catch
) - 支持
Promise.all
/Promise.race
等高级操作
-
缺点:
- 仍需处理
.then
链 - 无法取消(除非手动封装)
- 仍需处理
四. Async/Await:同步写法的异步魔法
async
声明一个函数是异步函数,写在函数声明之前,总是返回一个Promise ;await
只能在 async
函数中使用,暂停代码执行,等待 Promise 解决。
- 原理:Generator + Promise 的语法糖
- 代码示例:
JavaScript代码
// async await
function a() {
return new Promise(function(resolve,reject){
setTimeout(function () {
console.log("a");
resolve()
}, 1000)
}
)
}
function b(){
console.log('b');
}
async function fn(){
// await a() // await 只能操作promise对象,且promise执行成功了才能执行后面的
// b()
}
console.log(fn()); //Promise { undefined } async 相当于是new Promise
如代码所示,我们在使用async
声明fn函数,直接返回fn()
的调用结果,能够得到一个空的Promise对象,如图:
我们结合await
,需要在await后面接一个Promise,即我们接函数a,如上代码17-18行,运行结果如图: 为什么一开始打印的是
pending
状态的Promise呢?
因为await
后面接的Promise会覆盖async的函数的返回值,就是函数a。在同步代码a()执行时,a返回的Promise状态还没有变为resolved
状态,所以返回的是一个pending状态的Promise。
其次,await会等待后面接的Promise对象完全执行完成后,才会执行后面的代码b()。
-
优点:
- 代码像同步一样直观
- 错误处理更自然(
try/catch
) - 调试友好(可逐行执行)
-
缺点:
- 需在
async
函数中使用 - 底层仍是 Promise,理解成本略高
- 需在
五. 终极对比:谁才是"异步之王"?
维度 | Callback | Promise | Async/Await |
---|---|---|---|
可读性 | ❌ 差 | ✅ 较好 | ✅✅ 最佳 |
错误处理 | ❌ 手动检查 | ✅ .catch |
✅ try/catch |
调试难度 | ❌ 困难 | ⚠️ 中等 | ✅ 容易 |
兼容性 | ✅ 所有环境 | ✅ ES6+ | ✅ ES2017+ |
适用场景 | 传统库兼容 | 复杂异步逻辑 | 现代项目开发 |
六. 实战建议:如何选择?
- 用 Callback:维护旧代码或底层库开发(如 Node.js 核心模块)
- 用 Promise :需要并行处理多个异步任务(如
Promise.all
) - 用 Async/Await:绝大多数现代项目,尤其是逻辑复杂的场景
七. 结语:没有银弹,只有最合适的工具
- 技术选型需权衡项目需求、团队习惯和运行环境
- 未来趋势:Async/Await + Promise 组合为王
这期的分享就到这里啊哈哈哈,下次见,喜欢的话,记得点个赞点个关注喔~
