javascript事件循环
要理解js的事件循环,首先我们要清楚浏览器线程和js执行线程。
浏览器线程包含:
-
GUI渲染线程(负责解析HTML、CSS、构建dom树、布局和绘制)
-
事件触发线程(鼠标键盘事件、Ajax/Fetch异步请求),个人认为Ajax/Fetch异步请求是先在事件触发线程中等待管理,等到http请求被触发时再有异步请求线程进行控制。 如有理解错误还望指出。
-
定时触发器线程(管理定时器任务,待定时器计时结束后将回调函数传入消息队列等待js执行线程执行)
-
异步Http请求线程(XMLHttpRequest在连接后浏览器会新开一个线程进行管理,待请求状态改变后如果有回调函数会将回调函数传入消息队列等到js执行线程进行执行)
-
js引擎线程(负责执行js脚本代码,同一个tab页只有一个js引擎线程,所以不同tab页通信需要借助其他工具)
js引擎在处理事件时有一个循环事件处理模型。这个模型分为三个部分:事件执行栈、对象堆、消息队列。
- 事件执行栈遵循先进后出原则,js引擎会不断执行栈内的函数,直到栈空。
- 对象堆保存着变量对象。
- 消息队列遵循先进先出原则,在其他线程(定时触发器线程、事件触发线程、异步http请求线程)执行完的任务的回调函数会被放入对应消息队列等待事件执行栈空后将回调函数压入栈中等待js引擎进行处理。
在弄清楚浏览器各线程以及js引擎线程循环事件处理模型组成部分后我们需要理解js内部对事件的种类划分。

如图所示js的事件分为两大类:同步任务和异步任务,在异步任务中又分为宏任务和微任务。
事件执行顺序
同步任务执行顺序
javascript
function a(x) {
return 2*x;
}
function b(y) {
const double_y = 2 * y;
return a(double_y);
}
b(1);
在这段代码中创建了两个同步任务,函数a和函数b。在调用时我们首先调用了函数b,然后函数调用了函数a。
同步任务执行顺序是:
1、调用函数b时,将函数b压入栈,栈帧中包含函数b的参数引用和变量信息。
2、函数b又调用了函数a,所以将函数a压入栈中,a的栈帧包含函数a的参数引用和变量信息。
3、当函数a执行完毕后,a的栈帧就弹出栈。
4、执行函数b,函数b执行完后将b的栈帧弹出栈,此时栈内没有任何栈帧。
在js引擎中对代码的读取是从上至下读取,但是在执行事件时,如果都是同步代码,那么是谁先调用,谁先执行。如果有嵌套调用的情况,被调用的先完成执行,然后再返回上一层函数执行剩余代码。
同步任务和异步任务执行顺序
javascript
new Promise(function(resolve, reject){
console.log("this b")
resolve("this c")
}).then(val => {
console.log(val);
})
console.log("this a")
执行结果
javascirpt
this b
this a
this c
可能有朋友会好奇,为什么console.log("this a")
会比console.log("this b")
后执行。
因为在new Promise
时new一个Promise对象是一个同步任务,这个Promise实例调用的then方法才是一个异步任务。
所以js从上往下执行代码: 创建一个Promise实例对象 -> 执行同步console.log("this b") -> resolve被调用,作为异步任务先由事件触发线程进行管理 -> 执行同步任务 console.log("this a") -> 同步任务执行完成,将异步任务的回调函数由消息队列传入执行栈
同步任务和宏任务、微任务执行顺序
javascript
setTimeout(() => {
console.log("1");
}, 1000)
setTimeout(() => {
console.log("5");
}, 0)
new Promise(function(resolve, reject){
console.log("2")
resolve("3")
}).then(val => {
console.log(val);
})
console.log("4")
执行顺序
javascript
2
4
3
5
1
在这个示例中,添加了宏任务setTimeout
,不难发现,宏任务的执行顺序是低于微任务和同步任务的。
由这三个示例我们可以得出
执行顺序:
同步任务 > 微任务 > 宏任务
现在事件循环顺序理清楚后我们可以开始理解js事件循环模型。

-
js在执行js脚本时会将同步任务依次放入执行栈中,执行栈会持续对同步任务进行处理。在没有嵌套调用情况时,同步任务的执行顺序是从上至下,如果有嵌套情况会先将最里层嵌套函数执行完毕后再返回上层代码执行剩余部分以此类推直到执行完最表层代码。
-
在执行同步任务过程中如果遇到了异步任务,会将异步任务交由对应的线程进行处理,处理完后的异步任务如果有回调函数会放入消息队列中等待时间执行栈为空时进行读取。
之所以称之为事件循环,是因为消息队列经常按照类似如下的方式被实现:
javascript
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage() 会同步地等待消息到达 (如果当前没有任何消息等待被处理)。
-
在所有同步任务执行完后,js引擎线程会去扫描微任务消息队列是否有事件,如果会则将事件传入执行栈中进行执行。待当前所有微任务消息执行完后。
-
js引擎线程会去扫描宏任务消息队列,并传入一个宏任务到执行栈中,如果该宏任务执行过程中遇到了其他宏任务或微任务,会将该任务挂载到对应的消息队列中。一个宏任务执行后会立马扫描微任务消息队列是否有新的微任务,待到将微任务消息队列执行完后再传入下一个宏任务继续执行。
示例如下:
javascript
setTimeout(() => {
console.log("5")
}, 1000)
setTimeout(() => {
setTimeout(() => {
console.log("6");
}, 0)
new Promise(function(resolve, reject){
resolve(7)
}).then(val => {
console.log(val);
return val * 2;
}).then(val => {
console.log(val);
})
}, 0)
new Promise(function(resolve, reject){
console.log("2")
resolve("3")
}).then(val => {
console.log(val);
})
console.log("4")
执行结果
javascript
> "2"
> "4"
> "3"
> 7
> 14
> "6"
> "5"
宏任务类型
javascript
定时任务:setTimeout、setInterval
动画任务:requestAnimationFrame
常规任务:用户交互事件、页面渲染
文件任务:I/O读写
微任务类型
javascript
Promise相关:then/catch/finally
DOM监听:MutationObserver
API支持:queueMicrotask
总结
执行顺序:
1、同步任务:非嵌套情况下,同步任务从上到下执行。嵌套情况下,从内到外执行。
2、所有同步任务执行完后执行异步任务。
3、异步任务首先执行微任务,所有微任务执行完之后执行一个宏任务。
4、一个宏任务执行后如果有新的微任务则先执行微任务,否则执行下一个宏任务。