一、进程线程那点事儿:浏览器里的 "打工人天团"
咱们先从最基础的概念聊起。很多人写了几年 JS,还分不清进程和线程,就像分不清奶茶里的珍珠和椰果 ------ 虽然都在一个杯子里,但本质不一样。
-
进程:可以理解为 CPU 运行指令时加载和保存上下文的时间成本,相当于一个独立的 "工作车间"
-
线程:是 CPU 实际执行指令的时间,相当于车间里的 "打工人"
举个例子:当你多开一个浏览器标签页,就相当于新增了一个进程。每个进程里至少有这几个核心线程:
-
渲染线程(负责画页面)
-
JS 引擎线程(执行我们写的代码)
-
HTTP 请求线程(帮我们发网络请求)
这里有个关键知识点:JS 引擎线程和渲染线程是互斥的。就像同一时间只能有一个人用厕所,JS 在执行时浏览器没法没法渲染页面,渲染页面时 JS 也歇着 ------ 所以别写太耗时的同步代码,不然页面会卡死,用户体验堪比便秘。
二、V8 引擎:单线程的 "倔强打工人"
咱们写的 JS 代码主要靠 V8 引擎执行,这货有个特点:默认只开一个线程。
单线程意味着什么?所有任务得排队执行。就像只有一个收银台的超市,前面的人买完单,后面的才能上。但问题来了:如果前面有人买了 100 瓶矿泉水(比如一个耗时 3 秒的网络请求),后面的人岂不是要等到天荒地老?
于是,异步机制应运而生 ------ 这是单线程逼出来的智慧:
-
同步任务:直接执行(比如
console.log) -
异步任务:先放到任务队列排队,等主线程空闲了再处理(比如
setTimeout)
三、事件循环(Event Loop):异步任务的 "调度总指挥"
如果把 JS 执行过程比作演唱会,事件循环就是那个负责叫号的工作人员。它的核心工作就是协调同步任务、微任务、宏任务的执行顺序。
3.1 任务队列的 "阶级划分"
异步任务不是随便排队的,这里面有明确的 "等级制度":
-
微任务(VIP 队列):
-
Promise.then()、Promise.catch()等 Promise 回调 -
process.nextTick()(Node 环境,优先级比 Promise 还高) -
MutationObserver(监听 DOM 变化的 API)
-
-
宏任务(普通队列):
-
整个 script 标签的代码(最开始执行的宏任务)
-
setTimeout()、setInterval() -
AJAX 网络请求
-
I/O 操作(比如读写文件)
-
UI 渲染(浏览器负责,JS 只能触发不能直接控制)
-
3.2 执行顺序:牢记这个 "四字口诀"
事件循环的执行规则就像医院就诊流程,记住这四步准没错:
-
执行同步代码(属于宏任务的一种),过程中遇到异步任务就按类型放进对应队列
-
清空微任务队列:同步代码执行完,立刻把所有微任务按顺序执行完毕
-
可能的渲染:微任务全搞定后,浏览器可能会渲染一次页面(不是每次都有)
-
执行下一个宏任务:从宏任务队列里取一个任务执行,然后重复 1-3 步
四、async/await:披着同步外衣的异步 "卧底"
很多新手被async/await迷惑,以为它们是同步代码,其实这俩是Promise的语法糖,本质还是异步。咱们来拆解一下:
- async 函数 :在函数前面加
async,相当于给函数装上了 "自动包装器"------ 无论你 return 什么值,都会被包装成Promise.resolve(返回值)。比如:
js
async function fn() { return 1 }
// 等同于
function fn() { return Promise.resolve(1) }
- await 关键字 :必须和
async搭配使用,就像泡面和热水的关系。它的作用是:
-
等待后面的表达式执行(
如果是 Promise就等它状态变更,不是就直接拿结果) -
把自己后面的代码 "踢" 到
微任务队列里(这是关键!)
举个例子:
js
async function foo() {
await a() // 这里会暂停,把后续代码丢进微任务队列
b()
console.log('hello');
}
执行到await a()时,JS 会先执行a(),然后把b()和console.log('hello')打包扔进微任务队列,接着继续执行主线程的其他同步代码。
五、代码实测:用案例打脸 "想当然"
光说不练假把式,咱们用几个经典案例验证一下上面的理论。
案例 1:最基础的异步顺序
js
let a = 1
setTimeout(() => {
a = 2
}, 1000)
console.log(a) // 输出:1
解析 :setTimeout里的代码是异步宏任务,会在同步代码console.log(a)之后执行,所以打印 1 而不是 2。
案例 2:Promise 与 setTimeout 的较量
js
console.log(1);
new Promise((resolve) => {
console.log(2); // Promise构造函数是同步的
resolve()
})
.then(() => {
console.log(3); // 微任务
setTimeout(() => {
console.log(4); // 宏任务
}, 0)
})
setTimeout(() => {
console.log(5); // 宏任务
setTimeout(() => {
console.log(6); // 宏任务
}, 0)
}, 0)
console.log(7); // 同步代码
执行结果 :1 2 7 3 5 4 6
解析:
-
先执行同步代码:
1、2(Promise 构造函数)、7 -
清空微任务:
3 -
执行宏任务队列:先执行第一个
setTimeout打印5,再执行它里面的setTimeout(加入宏任务队列);然后执行then里的setTimeout打印4;最后执行最里面的setTimeout打印6
案例 3:多层 setTimeout 的执行顺序
js
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3) // 1秒后执行
}, 1000)
}, 0)
setTimeout(() => {
console.log(4) // 2秒后执行
}, 2000)
console.log(5);
执行结果 :1 5 2 3 4
解析:
-
同步代码先执行:
1、5 -
第一个
setTimeout(0 延迟)先进入宏任务队列,执行后打印2,同时把内部的setTimeout(1 秒)加入队列 -
第二个
setTimeout(2 秒)后进入队列,所以在3之后执行
案例 4:async/await 与 Promise 的混合战斗
js
console.log('script start');//1
async function async1() {
await async2()
console.log('async1 end'); // 5微任务
}
async function async2() {
console.log('async2 end'); // 2同步执行
}
async1()
setTimeout(() => {
console.log('setTimeout'); // 8宏任务
}, 0)
new Promise((resolve, reject) => {
console.log('promise'); // 3同步执行
resolve()
})
.then(() => {
console.log('then1'); // 6微任务
})
.then(() => {
console.log('then2'); // 7微任务
});
console.log('script end'); // 4同步执行
执行结果 :script start async2 end promise script end async1 end then1 then2 setTimeout
解析:
-
同步代码阶段:打印
script start→ 调用async1()执行async2()打印async2 end→ 遇到await,把async1 end丢进微任务 → 执行Promise构造函数打印promise→ 打印script end -
微任务阶段:先执行
async1 end→ 再执行第一个then打印then1→ 触发第二个then打印then2 -
宏任务阶段:执行
setTimeout打印setTimeout
案例 5:async 函数中的定时器
js
function a() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('a'); // 1秒后执行的宏任务
resolve()
}, 1000)
})
}
function b() {
console.log('b');
}
async function foo() {
setTimeout(() => {
console.log('c'); // 1.5秒后执行的宏任务
}, 1500)
await a() // 等待a()的Promise完成
b() // 微任务
console.log('hello'); // 微任务
}
foo()
执行结果 :a b hello c
解析:
-
调用
foo()后,先执行里面的setTimeout(1.5 秒)加入宏任务队列 -
执行
await a()时,先运行a()里的setTimeout(1 秒)加入宏任务队列 -
1 秒后,
a()的setTimeout执行,打印a并resolve,此时b()和console.log('hello')作为微任务执行 -
1.5 秒时,
foo()里的setTimeout执行,打印c
六、避坑指南:这些 "坑" 我替你踩过了
-
setTimeout (0) 不是立即执行:它只是告诉 JS"尽快",但必须等同步代码和微任务都执行完。实际延迟可能大于 0,取决于主线程忙碌程度。
-
Promise 构造函数是同步的 :只有
.then()、.catch()里的回调才是微任务。别看到new Promise就以为里面的代码是异步的。 -
多个 setTimeout 的执行顺序:不一定按代码顺序,取决于延迟时间和事件循环的时机。比如延迟 100ms 的可能比延迟 50ms 的后执行,如果前者先被加入队列但延迟更长。
-
async/await 的错误用法 :如果
await后面跟的不是 Promise,它就失去了 "等待" 的意义,后续代码会立即执行(相当于没加await)。 -
微任务的嵌套执行:在微任务里再添加微任务,会继续在当前微任务阶段执行,直到清空所有微任务。就像在医院急诊室里又新增了急诊病人,还是会优先处理。
七、总结:事件循环的 "一句话精髓"
同步代码先执行,遇到异步分两队(微任务、宏任务);同步干完清微队,微队清空看宏队,循环往复不停歇。
理解了事件循环,你会发现 JS 的异步行为其实非常有规律,就像广场舞大妈的舞步 ------ 看似杂乱,实则章法严明。下次再遇到代码执行顺序不符合预期,不妨画个任务队列图,一步一步分析,再复杂的情况也能迎刃而解~