前言
-
首先,JavaScript是一个单线程的脚本语言。
JavaScript是单线程的
,可以说这是JavaScript最核心也是最基本的特性。单线程就意味着同一时间只能干一件事 -
其次,JavaScript需要异步
如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验。
但JavaScript虽然是单线程,但是完全可以模拟多线程(借助于浏览器这个宿主来实现),靠的就是 事件循环
Event Loop
。所以JS中存在异步执行。
事件循环:
-
是单线程程序在处理异步事件时进行的一种循环过程。
-
对于异步事件,程序会先将其加入到事件队列中挂起,等主线程空闲时会去执行事件队列中的事件。这个过程会不断重复,形成一个循环。
JavaScript 是单线程语言,通过 事件循环(Event Loop) 实现异步编程。
事件循环的核心由 宏任务队列(MacroTask Queue) 和 微任务队列(MicroTask Queue) 组成,两者的执行顺序直接影响代码行为。
事件循环机制
1. 执行栈
当JavaScript开始执行代码时,会按顺序逐行执行,并形成一个执行栈。
执行栈是一个后进先出(LIFO)的数据结构,用于存储函数调用。
在执行过程中如果遇到:
- 同步任务:会按照代码顺序立即执行
- 异步任务:不会立即执行,而是会将这些任务放入 Event Table(事件表)中注册并等待执行。同时,继续执行后续的同步任务,不会阻塞主线程的执行。
直到所有的同步任务执行完毕,即主线程(执行栈)空闲
2. 消息队列
异步任务在 Event Table 中注册函数,当异步任务的回调函数被触发时,Event Table 会将这些任务推入 Event Queue(事件队列)中等待执行。异步任务又分为微任务和宏任务:
宏任务
宏任务是JavaScript中的最外层任务,通常包括一些执行时间较长、可能阻塞线程的操作。
特点:
- 宏任务由浏览器或Node.js环境发起的任务,会在每轮事件循环中依次执行,每次事件循环从宏任务队列中取 一个 任务执行。
- 宏任务队列可以有多个,JavaScript引擎会按照先进先出的顺序处理。
宏任务会被添加到宏任务队列(MacroTask Queue)中,常见的宏任务有:
<script>
标签整体代码 (全局代码的执行本身是一个宏任务)setTimeout
、setInterval
(定时器回调)I/O 操作
(如文件读写、网络请求等)- UI 渲染:浏览器在更新DOM或样式后,会触发重绘(Repaint)和回流(Reflow)等UI渲染过程
- 事件回调:如点击、滚动等用户交互事件
.
微任务
微任务是在当前宏任务执行完毕后、下一个宏任务开始前立即执行的一类任务。
特点:
- 微任务由 JavaScript 自身发起的任务,每个宏任务执行完毕后会立即清空所有微任务队列。微任务通常用于处理一些比宏任务优先级更高的操作,确保任务能够快速完成。
- 微任务队列只有一个,所有微任务会按照添加的顺序依次执行。
微任务会被添加到微任务队列(MicroTask Queue)中,常见的微任务有:
Promise
的回调函数(即then
、catch
、finally
中的函数)MutationObserver
用于观察DOM树的变化,并在变化发生时执行回调函数(DOM 变动监听回调)process.nextTick
(Node.js 特有,优先级高于其他微任务)queueMicrotask
一个直接将任务添加到微任务队列的函数,适用于需要在当前事件循环周期内尽快执行的任务(显式添加微任务的 API)
宏任务(MacroTask) | 微任务(MicroTask) | |
---|---|---|
定义 | JavaScript 中的最外层任务,执行时间较长,可能阻塞线程 | 在当前宏任务执行完毕后、下一个宏任务开始前立即执行的任务 |
特点 | - 由浏览器或 Node.js 环境发起 - 每轮事件循环中依次执行一个 - 宏任务队列可以有多个,先进先出顺序处理 | - 由 JavaScript 自身发起 - 每个宏任务执行完毕后会立即清空所有微任务队列 - 微任务队列只有一个,按照添加顺序依次执行 |
常见类型 | - <script> 标签整体代码- setTimeout 、setInterval (定时器回调)- I/O 操作(如文件读写、网络请求等) - UI 渲染(重绘、回流等) - 事件回调(如点击、滚动等用户交互事件) | - Promise 的回调函数(then 、catch 、finally )- MutationObserver (DOM 变动监听回调)- process.nextTick (Node.js 特有)- queueMicrotask (显式添加微任务的 API) |
队列 | 先进先出 FIFO | 先进先出 FIFO |
这个表格清晰地展示了宏任务和微任务的定义、特点以及常见类型,方便进行对比和理解。
微任务和宏任务是分开存储在不同的队列中的,但它们都通过事件循环来调度和执行。
3. 事件循环
当所有的同步任务执行完毕,即主线程(执行栈)空闲时,事件循环(Event Loop)会检查 Event Queue(消息队列)中是否有待执行的异步任务。
如果有,事件循环会取出一个异步任务(通常是消息队列中的第一个任务)放入主线程执行栈中执行:
-
JavaScript引擎会先查看微任务队列是否有任务。如果有,就执行微任务队列中的所有任务,然后再继续查看微任务队列,直到微任务队列为空。
-
执行完所有的微任务后,从宏任务队列中取出下一个宏任务,取出到执行栈中执行。
-
这个过程会不断重复,从而形成“事件循环”。
在一个事件循环的迭代中,微任务总是先于宏任务执行,会先执行完所有的微任务,然后才会执行下一个宏任务。并且在一个宏任务执行完毕后,会立即检查并执行微任务队列中的所有任务。
执行的顺序是 执行栈中的代码 => 微任务 => 宏任务 => 微任务 => 宏任务 => ...
。
事件循环是 JavaScript 运行时不断监视执行栈和Event Queue(消息队列)的过程,负责协调同步任务和异步任务的执行
流程图
总结
综上所述,JavaScript 的事件循环机制负责协调同步任务和异步任务的执行。在主线程空闲时,事件循环会从消息队列中取出一个异步任务放入主线程中执行。在执行异步任务的过程中,可能会产生新的微任务,这些微任务会被放入微任务队列中等待处理。事件循环会优先处理微任务队列中的所有微任务,然后再处理消息队列中的一个消息。这样可以确保 JavaScript 代码的执行顺序符合预期。
问题
-
微任务中又产生了新的微任务,新产生的微任务会什么时候执行?
当前的微任务执行完后,事件循环会接着从微任务队列的头部取出下一个微任务来执行,直至微任务队列为空,然后才会去执行下一个宏任务。
新产生的微任务会在当前微任务队列的末尾排队,在当前这一批微任务依次执行完毕前执行,而不是等到下一个宏任务执行时才执行。
-
微任务中又产生了新的宏任务,新产生的宏任务会什么时候执行?
- 新宏任务会被添加到宏任务队列的末尾。
- 继续执行当前微任务队列中的剩余微任务,直到所有微任务执行完毕。
- 事件循环才会从宏任务队列中取出下一个宏任务执行。
- 由于新产生的宏任务是后加入队列的,会在队列中已有的其他宏任务之后执行(宏任务会按照队列中 先进先出(FIFO) 的顺序依次执行)
.
新的宏任务会被添加到宏任务队列中,但不会立即执行。它会等到当前微任务队列全部执行完毕,并且下一个宏任务开始时才会被处理。
举例
例1:
console.log(1)
setTimeout(function(){
console.log(2)
},0)
console.log(3)
运行结果是: 1 3 2
它的执行顺序:
console.log(1)
是同步任务,放入主线程里setTimeout()
是异步任务,被放入event table
, 0 秒之后被推入event queue
里console.log(3)
是同步任务,放到主线程里- 先执行完所有的同步任务,即先打印出
1
和3
- 执行栈空闲时再去
event queue
里取出一个异步任务放入主线程执行栈中执行,即打印出2
.
例2:
setTimeout(function(){
console.log(1)
});
new Promise(function(resolve){
console.log(2);
resolve();
}).then(function(){
console.log(3)
});
console.log(4);
运行结果是: 2 4 3 1
也就是说 promise 在 setTimeout 之前执行了。
对例2的分析:
- 首先执行
script
下的宏任务,遇到setTimeout
,将其放到宏任务的【队列】里 - 遇到
new Promise
直接执行,打印 “2” - 遇到
then
方法,是微任务,将其放到微任务的【队列里】 - 遇到
console.log(4)
,同步任务,直接打印“4” - 打印 “代码执行结束”
- 本轮宏任务执行完毕,查看本轮的微任务,发现有一个
then
方法里的函数, 打印"3" - 到此,本轮的
event loop
全部完成。 - 下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个
setTimeout
里的函数,执行打印"1" - 所以最后的执行顺序是: 2 4 3 1
注意:
DOM事件也是基于Event Loop,但不是异步