事件循环之浏览器事件循环
date
Mar 21, 2022
slug
event-loop-in-browsers
status
Published
tags
Interview
Browsers
summary
事件循环之浏览器事件循环
type
Post
概念
事件循环是什么?
- 可以把事件循环理解成我们编写的 JavaScript 和浏览器或者 Node 之间的一个桥梁。
- 浏览器的事件循环是我们编写的 JavaScript 代码和浏览器API调用(setTimeout|AJAX|监听事件等)的一个桥梁,桥梁之间他们通过回调函数进行沟通。
- Node 的事件循环是一个我们编写的 JavaScript 代码和系统调用(file system|network等) 之间的一个桥梁,桥梁之间他们通过回调函数进行沟通。
进程和线程
- 线程和进程是操作系统中的两个概念:
- 进程( process ): 计算机已经运行的程序;
- 线程( thread ): 操作系统能够运行运算调度的最小单位;
- 简单解释:
- 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
- 线程:每一个进程中,都会启动一个线程来执行程序中的代码,这个线程被称之为主线程;
- 所以我们也可以说进程是线程的容器;
- 生活例子类比:
- 操作系统类似于一个工厂
- 工厂中有很多的车间,这个车间就是进程
- 每个车间可能有一个以上的工人在工厂,这个工人就是线程
多进程多线程开发
- 操作系统是如何做到同时让多个进程(边听歌,边写代码,边查阅资料)同时工作的呢?
- 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
- 当我们的进程中的线程获取到时间片时,就可以快速的执行我们编写的代码
- 对于用户来说是感受不到这种快速切换的
浏览器和JavaScript
- 我们经常说JavaScript时单线程的,但是JavaScript 的线程应该有自己的容器进程:浏览器或者 Node
- 浏览器是一个进程吗,它里面只有一个线程吗?
- 目前多数的浏览器其实是多进程的,当我们打开一个tab页面时就会开一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
- 每个进程中又有很多的线程,其中包括执行 JavaScript 代码的线程
- 但是 JavaScript 的代码执行是在一个单独的线程中执行的
- 这就意味着 JavaScript 的代码,在同一个时刻只能做一件事
- 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞
JavaScript执行流程
- 对于普通的 JavaScript 代码的执行,会有一个函数调用栈的概念,调用的函数会被压入到栈中,先进栈的后出栈。
浏览器的事件循环
- 如果在执行 JavaScript 代码的过程中,有异步操作呢?
- 在代码执行过程中插入一个 setTimeout 函数的调用
- 这个函数被放到调用栈中,执行会立即结束,并不会阻塞后续代码的执行
- 那么,传入的函数,会在什么时候被执行呢?
- 事实上,setTimeout是调用了 web api (计时器线程负责),在合适的时机,会将回调函数加入到一个事件队列中
- 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行
宏任务与微任务
- 但是事件循环中并非只维护着一个队列,事实上浏览器中是有两个队列:
- 宏任务队列(macrotask queue): ajax | setTimeout |setInterval | DOM监听 | UI Rendering 等
- 微任务队列(microtask queue): Promise的then 回调,Mutation Observer API | queueMicroTask() 等
- 那么事件循环对于两个队列的优先级是怎么样的呢?
- main script 中的代码优先执行(编写的顶层 script 代码)
- 在执行任何一个宏任务之前(不是队列,是一个宏任务), 都会先查看微任务队列中是否有任务需要执行
- 也就是宏任务执行之前,必须保证微任务队列是空的
- 如果不为空,那么就优先执行微任务队列中的任务(回调)
⚠️提示
async、await 是Promise的一个语法糖:
- 我们可以将awit 关键字后面执行的代码,看作是包裹在 (resolve, reject) ⇒ {函数执行} 中的代码,类似于new Promise 的构造函数,就是立即执行
- awit 的下一条语句,可以看作是 then(res ⇒ {函数执行}) 中的代码
练习
习题一:
setTimeout(function(){
console.log('set1');
new Promise(function(resolve){
resolve();
}).then(function(){
new Promise(function(resolve){
resolve();
}).then(function(){
console.log('then4')
});
console.log('then2')
})
})
new Promise(function(resolve){
console.log('pr1')
resolve()
}).then(function(){
console.log('then1')
});
setTimeout(function(){
console.log('set2')
});
console.log(2)
queueMicrotask(() => {
console.log('queueMicrotask1')
});
new Promise(function(resolve){
resolve();
}).then(function(){
console.log('then3')
})- 分析
- 首先第一部分是一个setTimeout 函数,因此他的回调函数将会被放到宏任务队列中,等待被执行。
- 然后是一个new Promise, 它的构造函数将会被立即执行,因此首先输出打印 pr1,紧接着resolve() 被执行,对应的then 函数回调 then1 被放到了微任务队列。
- 接着还是个setTimeout。同样的,它的回调函数会被添加到宏任务队列,此时没有输出打印。
- 接着是一个正常的log函数执行,此时输出打印2。
- 接下来是一个queueMicotask函数,它的回调函数将会被添加到微任务队列,没有额外输出打印。
- 然后又是一个new Promise, 它的构造函数 resolve() 将会被立即执行,因此对应的then 函数回调会被添加到微任务队列,没有额外输出打印。
- 此时main script 全部执行完毕,接下来将会优先查看微任务队列中是否有回调函数需要执行,此时微任务队列中有 then1 queuetask1 then3 三个回调函数,因此将依次执行这三个函数,则输出打印 then1, queuetask1, then3。
- 当前微任务队列已空,则查看宏任务队列,执行 set1 回调函数,首先执行log(’set1’), 则输出打印set1, 然后继续执行 new Promise 的构造函数resolve(),将对应的then 回调函数添加到微任务队列,暂且将这个回调函数命名为cb1。
- 此时微任务队列中有cb1 回调函数,因此优先执行cb1 函数。 则 new Promise 的构造函数resolve()被执行,对应的then 回调函数 then4 被添加到了微任务队列,然后继续执行后面的log(then2),则输出打印then2。
- 此时微任务队列中还有then4 这个回调函数,则执行函数,输出打印then4。
- 最后,微任务队列为空,宏任务队列中只剩set2 函数,则执行set2, 输出打印set2。











- 结论
- 最后的输出结果就是:pr1 2 then1 queuetask1 then3 set1 then2 then4 set2
习题二:
async function async1() {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')- 分析
- 首先是async1 和async2 两个函数的定义,然后是log函数执行,则输出打印 script start, 接下来代码继续执行 setTimeout,将setTimeout 回调函数添加到宏任务队列。
- 接着是async1 函数被调用,则执行打印,输出打印 async1 start, awit 后面的 async2 函数被调用且立即执行,则输出打印 async2。 awit 下面一句则相当于then 函数回调,它会被添加到微任务队列,此时async1 函数调用结束。
- 接下来是 new Promise, 则立即执行构造函数,输出打印 promise1, 然后执行resolve(), 即将后面then 的回调函数添加到微任务队列。然后执行最后一个打印语句,则输出打印 script end。 main script 执行完毕。
- main script 执行完毕之后则优先检查微任务队列是否有回调函数,依次执行,因此打印输出 async1 end, 然后再是 promise2。
- 最后微任务队列中也为空,则查看宏任务队列,执行setTimeout 回调函数,则打印输出setTimeout。





- 结论
- 最后输出的结果就是: script start, async1 start, async2, promise1, script end, async1 end, promise2 setTimeout