前言
在JavaScript开发中,异步是重中之重,也是前端入门阶段最难理解的知识点之一。从基础的同步/异步区别,到JS单线程底层原理,再到最基础的回调函数,都是我们进阶学习Promise的基石。本文结合通俗案例,拆解JS异步的底层逻辑与回调相关知识,帮大家从零吃透相关知识点。
1. 同步与异步
1.1 同步
代码按照一个从上到下依次执行,这就是同步执行。
写一段代码解释:
js
console.log('第一个执行')
console.log('第二个执行')
console.log('第三个执行')
console.log('第四个执行')
正如我们所想的一样,他会按上到下顺序执行:

1.2 异步
异步就是在执行的过程中,如果碰到一个耗时的任务,就分精力出来执行另外一个任务。
举个生活中的例子:
假设小明 7 点起床,前往学校需要 20 分钟,手头还有多项家务:煮饭 20 分钟、烧水 10 分钟、洗衣服 10 分钟、洗漱 5 分钟、晾衣服 3 分钟。想要省时不迟到,最优安排就是先启动煮饭和烧水这两项耗时较长的事,在它们运行的间隙,同步完成洗衣、洗漱等其他工作。这和代码里的异步原理完全一致,面对耗时操作时,合理穿插执行其他任务,提升整体效率。
用代码让你加深异步理解:
js
let a = 1; //同步代码 或者叫同步任务
setTimeout(() => { //异步任务,应该被挂起
a = 2;
}, 1000);
console.log(a);
此时输出会打印 1 还是 2 呢?知道了异步的概念后,我们应该就知道打印的a应该是 1 :

2. 单线程原理
TS 默认是单线程执行的
2.1 进程与线程
进程:CPU从接收到一个指令,到加载完上下文所需要的时间。
线程:CPU执行指令所需要的时间。
举个例子方便你的理解:
当我们新开一个浏览器的 tab 页这其实就是一个进程,而这个进程是由多个线程通力合作完成的(如HTTP 网络线程, 页面渲染线程)。
启用 node 来运行 js ,这就是一个进程,但是 v8 会默认开启一个线程来执行代码,所以js代码在遇到一个耗时的任务时,就会将它挂起,先去执行那些不耗时的任务。
3. 回调
我们知道,JavaScript 在执行代码时,遇到耗时任务会将其挂起,优先执行不耗时的同步任务。
这里就会出现一个很典型的问题:假设 A 函数是向后端请求数据的耗时任务,B 函数需要拿到 A 请求到的数据才能渲染页面。我们按正常顺序先调用 A、再调用 B,由于 JS 是单线程异步执行,引擎会先执行不耗时的 B 函数,再回头处理 A 的数据请求。
js
let a = null
function A() {
setTimeout(() => {
a = 100
}, 1000)
}
function B() {
console.log(a);
}
A()
B()

这就会导致 B 拿不到数据,页面渲染失败,显然不是我们想要的结果。 那该怎么解决呢?其实思路很简单:让 B 函数等 A 函数请求完成后再执行 。最直接的方式,就是把 B 函数写到 A 函数的内部,等 A 拿到数据后再触发 B。这种 "把一个函数作为参数传给另一个函数,在合适的时机才执行" 的机制,就是回调。早期的 JavaScript 开发者,正是用这种回调方式来处理异步依赖问题。如以下代码:
js
let a = null
function A() {
setTimeout(() => {
a = 100
B()
}, 1000)
}
function B() {
console.log(a);
}
A()

3.1 基本概念
定义:当 B 函数需要依赖异步 A 的结果, 我们将 B 函数的调用放在 A 里面。
3.2 回调地狱
当 A 函数依赖 B 函数的结果,C 函数又依赖 A 函数,D 函数再依赖 C 函数...... 像这样层层依赖、一直嵌套到几十上百个函数时,代码会不断向右缩进,变成一团缠绕不清的结构。不仅可读性极差,出了问题也很难定位,维护成本极高,这就是典型的回调地狱。
3.3 Promise
为了避免回调地狱,JS官方引入了Promise;Promise 就是一个 "承诺" :它接收一个异步任务,向你保证 ------等任务做完了,我再告诉你结果是成功还是失败。
接下来,我就详细讲讲它的具体用法:
js
function xq() {
// Promise 内部拥有一个状态 statue = pending
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log('刘局相亲成功');
resolve()
}, 2000);
})
}
function marry() {
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log('刘局结婚了');
resolve()
}, 1000);
})
}
function baby(){
console.log('小刘出生')
}
xq().then(() => {
marry().then(() => {
baby();
});
});
我们来看看刘局从相亲 -> 结婚 -> 生娃的一个过程,很明显这是一个相亲和结婚是异步环节,只有先相亲,再结婚,最后再生娃,相比于之前那种老套的回调,Promise提供了更为简洁的、可读性更强方法。

为了让代码不会一直向右推进,我们可以这样改:
js
xq()
.then(() => {
return marry();
})
.then(() => {
baby();
})
很明显,这样的写法让代码更加扁平化、结构更清晰,从上到下依次执行,不会出现层层嵌套的情况,可读性和维护性都大大提升。
下面我们结合这个例子,深入看看 Promise 的底层运行原理:
js
function Promise(fn) {
this.state = 'pending' // 准备状态
this.arr = [foo]
const resolve = (res) => {
this.state = 'resolved' // 成功状态
// foo(res)
}
const reject = () => {}
fn(resolve, reject)
}
new Promise((resolve, reject) => {
resolve()
})
执行步骤:
- new Promise() 得到了一个状态为 pending 的对象 。
- then(foo) then 会将 foo 存起来。
- 时间到达,resolve 被触发。
- 将 Promise 内部的状态更改为 成功(resolved), 将当初 then 存起来的 foo 函数也触发。
Promise 还提供了 .catch() 方法,专门用来捕获异步任务执行失败时的错误信息。
js
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err); // 异步任务失败时,会执行这里的代码
});
这里的 res 和 err 并不是凭空产生的,它们的值是我们在创建 Promise 时,通过 resolve('Yes') 和 reject('No') 主动传递出来的参数。
简单来说,它们的对应关系是这样的:
js
res = 'Yes'; // res 接收来自 resolve() 的数据
err = 'No'; // err 接收来自 reject() 的数据
4. 总结
-
同步与异步:同步代码自上而下顺序执行;异步遇到耗时任务会将其挂起,优先执行其他代码,避免阻塞。
-
进程与线程:进程是资源分配单位,线程是指令执行单位。JS 基于 V8 引擎单线程运行,这也是异步机制存在的根源。
-
回调函数 :将后续逻辑放入异步任务内部,保障执行顺序;多层嵌套依赖会形成回调地狱,代码难读、难维护。
-
Promise :专为解决回调地狱而生的异步方案,拥有
pending、resolved、rejected三种状态。通过resolve配合.then()处理成功逻辑,reject配合.catch()捕获异常,支持链式调用,代码结构更简洁。