学习步骤如下, 在最后的解决问题中,会根据实际问题描述 Event loops在工作中是如何进行任务队列的推入和执行
一、一些问题
一、以下结果为什么输出结果是 b a
setTimeOut(() => {
console.log('a')
}, 0)
console.log('b')
二、我们看两段代码,代码都是由三块组成
- 初始时间
- 循环,与初始时间相差2s跳出循环,并输出当前时间
- 设置定时间,间隔1s, 函数体为输出当前时间和初始时间的差值
两段代码的区别是,第一段代码书写顺序是 1-2-3,第二段代码书写顺序是1-3-2
代码1
// 代码1
const startTime = new Date().getSeconds() // 初始时间
while(true) { // 与初始时间相差2s跳出循环
if (new Date().getSeconds() - startTime >=2) {
console.log('while', new Date().getSeconds() - startTime)
break
}
}
setTimeout(() => { // 设定定时器
console.log('setTimeout', new Date().getSeconds() - startTime)
}, 1000)
代码2
// 代码2
const startTime = new Date().getSeconds() // 初始时间
setTimeout(() => { // 设定定时器
console.log('setTimeout', new Date().getSeconds() - startTime)
}, 1000)
while(true) { // 与初始时间相差2s跳出循环
if (new Date().getSeconds() - startTime >=2) {
console.log('while', new Date().getSeconds() - startTime)
break
}
}
输出结果是
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
代码1输出
代码2输出
二、EventLoop 概念
在文章 In depth: Microtasks and the JavaScript runtime environment
中有一段对于js运行环境 和 eventLoop 的描述,如下
Run, JavaScript, run
在执行 JavaScript 代码的时候,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的代理。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 workder 的额外的线程集合、一个任务队列以及一个微任务队列构成。除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其它组成部分对该代理都是唯一的。
事件循环(Event loops)
每个代理都是由事件循环驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。
网页或者 app 的代码和浏览器本身的用户界面程序运行在相同的 线程中, 共享相同的 事件循环。 该线程就是 主线程,它除了运行网页本身的代码之外,还负责收集和派发用户和其它事件,以及渲染和绘制网页内容等。
~
~
~
从以上的描述我们可以得出结论,
- 事件循环主要有两个功能
1.执行所有处于等待中的 JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作(Event loops 会不断循环这个过程)
2.负责收集事件(包括用户事件以及其他非用户事件等)
- Event loops会将收集的任务分为 任务(宏任务)和微任务,然后排队,进行执行
三、一些基本概念
1.队列
队列可以认为是一个两段开口的管道,其中内容流动方向固定,遵循先进先出的规则
2.setTimeOut, setInterval 参数 delay(可选)
该参数表示定时器执行后,其中的函数体延迟执行的毫秒数 (一秒等于1000毫秒)
个人理解,这个参数表示,定时器执行后,将其中的函数体推入到事件队列的延迟时间
四、任务队列和微任务队列
从Event loops的概念中,我们知道,任务队列可以分为 任务队列(宏任务队列)和微任务队列
两者的区别在于
- 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行.
- 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。
简单来说,宏任务事件队列在执行前就会确定好要执行的 任务有哪些,这些任务执行完毕后,会去微任务队列执行微任务,微任务如果在执行过程中有新的微任务产生,Event loops 会把所有的微任务都执行完以后,再去宏任务队列中查看是否有可执行的宏任务
五、Event loops执行顺序
- 将代码分为宏任务和微任务,并分别添加到任务队列中
- 执行宏任务队列中的任务,并遵循规则,即使在执行过程中有新的宏任务添加到队列中,也不在这次执行范围
- 执行微任务队列,并遵循规则,如果执行过程中有新的微任务添加到队列,也执行,直到微任务队列为空
- 重复1-2-3-4
六、解决问题
1.第一个问题
// 宏任务 A 定时器
setTimeOut(() => { // 宏任务 B ,定时器的 执行函数
console.log('a')
}, 0)
// 宏任务 C
console.log('b')
按照Event loops 的执行流程
1. 将宏任务推送到宏任务队列,得到宏任务队列
A(定时器) | C(输出b) |
---|
2. 确认了本次事件循环执行宏任务为 A C
执行宏任务 A ,0s后推入定时器函数体宏任务 B
得到宏任务队列,但由于宏任务B 是事件循环执行后放入的,所以在下个事件循环执行
A(定时器) | C(输出b) | B(定时器回调函数) |
---|
执行宏任务 C, 输出结果 b
3.微任务队列没有任务,开始第四步
4.目前任务队列为
B(定时器回调函数) |
---|
执行宏任务 B ,输出结果 a
2.同样的,我们按照这个逻辑来处理代码1 和 代码 2
代码中有任务如下
宏任务 A 设置初始时间
宏任务 B 循环
宏任务 C 设定定时器
宏任务 D 定时器回调函数
代码1
// 代码1
const startTime = new Date().getSeconds() // 初始时间
while(true) { // 与初始时间相差2s跳出循环
if (new Date().getSeconds() - startTime >=2) {
console.log('while', new Date().getSeconds() - startTime)
break
}
}
setTimeout(() => { // 设定定时器
console.log('setTimeout', new Date().getSeconds() - startTime)
}, 1000)
则代码1 执行顺序为
1. 将宏任务推送到宏任务队列,得到宏任务队列
A(设置初始时间) | B(循环) | C(设定定时器) |
---|
2. 确认了本次事件循环执行宏任务为 A B C
执行宏任务 A
执行宏任务 B
2s后,满足跳出循环条件,输出结果while 2,宏任务B 结束
执行宏任务 C
1s 后,将事件D 推入宏任务队列,得到宏任务队列
D(定时器回调函数) |
---|
由于宏任务D 是事件循环开始后添加到宏任务队列的,所以任务D 在下次事件队列中执行
3. 检测微任务队列为空
4.检测宏任务队列为
D(定时器回调函数) |
---|
执行任务 D,输出结果 setTimeOut 3
代码2
// 代码2
const startTime = new Date().getSeconds() // 初始时间
setTimeout(() => { // 设定定时器
console.log('setTimeout', new Date().getSeconds() - startTime)
}, 1000)
while(true) { // 与初始时间相差2s跳出循环
if (new Date().getSeconds() - startTime >=2) {
console.log('while', new Date().getSeconds() - startTime)
break
}
}
代码2 执行顺序为
1. 将宏任务推送到宏任务队列,得到宏任务队列
A(设置初始时间) | C(设定定时器) | B(循环) |
---|
2. 确认了本次事件循环执行宏任务为 A C B
执行宏任务 A
执行宏任务 C
执行宏任务 B
宏任务 C 执行 1s后,将宏任务D 推入宏任务队列,目前任务队列为
B(循环) | D(定时器回调函数) |
---|
执行宏任务 B执行 2s后,满足跳出循环条件,输出结果while 2,宏任务B 结束
由于宏任务D 是事件循环开始后添加到宏任务队列的,所以任务D 在下次事件队列中执行
3. 检测微任务队列为空
4.检测宏任务队列为
D(定时器) |
---|
执行任务 D,输出结果 setTimeOut 2
3.我们来看一道面试题,
面试题
setTimeout(() => {
console.log('setTimeOut');
}, 0)
console.log('start')
new Promise(reslove => {
console.log('promise');
reslove()
}).then(() => {
console.log('then one');
}).then(() => {
console.log('then two');
})
console.log('end');
结果
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
⬇️
面试题中有任务如下
宏任务 A 设定定时器
宏任务 B 输出start
宏任务 C 创建Promise
微任务 D promise的then回调
微任务 D1 promise的then的回调的then的回调
宏任务 E 输出end
宏任务 F 定时器回调函数
面试题1.0 执行顺序为
1. 将宏任务推送到宏任务队列,得到宏任务队列
A(设定定时器) | B(输出start) | C(创建Promise) | E(输出end) |
---|
由于此时promise中的resolve并没有执行,微任务D暂时没有推入到微任务队列
2. 确认了本次事件循环执行宏任务为 A B C E
3.执行宏任务 A ,并在0s后将宏任务放入E推入宏任务队列,则任务队列为
A(设定定时器) | B(输出start) | C(创建Promise) | E(输出end) | F(定时器回调函数) |
---|
由于宏任务 F 是事件循环执行后放入宏任务队列的,所以在下一次事件循环中执行
4.执行宏任务 B,输出结果 start
5.执行宏任务 C,输出结果promise,将微任务 D promise的then回调推入到微任务队列,此时,微任务队列变为
D(promise的then回调) |
---|
6.执行宏任务 E,输出结果 end
此时,宏任务队列执行完毕,宏任务队列和微任务队列分别为
宏任务队列
F(定时器回调函数) |
---|
微任务队列
D(promise的then回调) |
---|
7.宏任务队列执行完毕,执行微任务队列中的 D,输出结果 then one,同时向微任务添加微任务 D1,遵循规则 事件循环执行微任务队列时,会把所有的微任务执行完毕,直至微任务队列为空,则 微任务队列继续执行 ,执行 D1, 输出结果 then two
8.此时,微任务队列为空,检测宏任务队列,发现不为空,执行宏任务队列 F , 输出结果 setTimeOut
面试题 2.0
//请写出输出内容
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');