微任务,宏任务与Event-Loop ——js的运行机制篇part1

从同步事件与异步事件的执行入手,详细剖析微任务,宏任务,了解js的事件循环机制

Posted by AzirKxs on 2022-07-09
Estimated Reading Time 9 Minutes
Words 2.4k In Total
Viewed Times

微任务,宏任务与Event-Loop

参考:

稀土掘金:
IT老班长 链接:https://juejin.cn/post/6945319439772434469
张倩qianniuer 链接:https://juejin.cn/post/6844903638238756878
Jiasm 链接:https://juejin.cn/post/6844903657264136200
小浪哥 链接:https://juejin.cn/post/6844903609990119431

ps:我只是通过自己学习这几篇文章进行了一个总结,另外根据自己的理解进行了拓展,仅仅是一个学习笔记,感谢大佬们写的优质文章,建议去阅读原文

前言

JavaScript是一个单线程的脚本语言。如果在一段代码的执行过程中,必然不会出现同时执行的零一行代码,例如学校打饭排队的窗口,一个一个顺序执行。因此如果一段代码的前面有高耗时的操作就会造成进程的阻塞问题,后面的代码一直在等待执行,页面处于了一个假死状态,因此需要异步来处理那些高耗时的操作。

JavaScript的两种执行模式

Js有两种执行模式,分为同步模式与异步模式,当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。

同步模式

按照顺序执行,前面的代码没有完成,后面的代码就不会执行,这样的代码会引发进程阻塞,页面假死的问题。

异步模式

什么是异步? 例如发送了一个网络请求,不必等待网络请求处理完成再继续执行代码,我们可以告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。
在异步完成时通知了主程序,但是主程序此时正在忙别的事情,这时候要等主程序忙完,等待主程序空闲下来再去执行这些异步请求。
比如说放学后和你一起回家的朋友有点别的事情需要去处理让你等他,你这时候不必闲着,去网吧打了会lol,当他忙完事情后,如果你的这一盘游戏还没有结束,他需要等待你打完这一盘你们才能一起回家。

更进一步

同步任务与异步任务的详细执行方式

1-1

同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
当指定的事情完成时,Event Table会将这个函数移入Event Queue。
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)。

此外,怎样直到主线程执行栈为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

1
2
3
4
5
6
7
8
9
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');

上面是一段简易的ajax请求代码:

1.ajax进入Event Table,注册回调函数success。
2.执行console.log(‘代码执行结束’)。
3.ajax事件完成,回调函数success进入Event Queue。
4.主线程从Event Queue读取回调函数success并执行。

宏任务与微任务

异步任务主要分为宏任务与微任务两种,他们的区别主要在于执行顺序,Event Loop的走向和取值

1-2

一个掘金的老哥(ssssyoki)的文章摘要:那么如此看来我给的答案还是对的。但是js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。 我当时看到这我就服了还有这种骚操作。

浏览器的事件循环是由:一个宏任务队列+宏任务队列中每个宏任务搭配的一个微任务队列们组成

执行顺序问题

1-3

看接下来一个案例来明白宏任务与微任务

案例1:

1
2
3
4
5
6
7
8
9
10
setTimeout(_ => console.log(4))

new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})

console.log(2)

执行后的打印顺序为 1,2,3,4

微任务是可以优先得到执行的异步任务,可以理解为插队

一种比较妥当的说明:

1.js代码整个为一个宏任务,现在开始执行
2.setTimeout为宏任务,现在已经运行了一个宏任务,放到宏任务事件队列中排队,需要放到第二轮执行
3.现在开始执行里面的同步任务,resolve()为同步任务,直接执行,打印1,console.log(2)为同步任务,打印2,
4.遇到微任务,将微任务放到微任务队列中排队等待,微任务只有等到上一轮的宏任务结束才能运行
5.上一轮宏任务结束,微任务开始执行,Promise.then执行,打印3
6.微任务执行完毕,开始执行下一轮的宏任务setTimeout,打印4

排前面的 script 先执行,执行其内部的【同】,再执行其【微】,接着就轮到下一个大的宏,也就是执行下一个 script,【同】、【微】。。。顺序执行完后,再从头开始,看第一个 script 是否有需要执行的【宏】,再去下一个 script 中找 【宏】,等大家宏结束后,进入下一轮循环。

为了验证我做了如下测试,下面验证了上面的说法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
setTimeout(function(){
console.log('我是第一段script中的宏任务');
},0)
Promise.resolve().then(()=>{
console.log('我是第一段script中的微任务');
})
console.log('我是第一段script中的同步任务');
</script>
<script>
console.log('我是第二段script中的同步任务');
Promise.resolve().then(()=>{
console.log('我是第二段script中的微任务');
})

console.log('我是第二段script中的同步任务');
</script>

1-4

7.12更新:我发现了问题,调查之后发现原来微任务与微任务之间是顺序执行,而宏任务与宏任务之间则不一定,为了验证宏任务之间的执行顺序,我做了如下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
setTimeout(function () {
console.log('我是第一段script中的宏任务1,延迟3000ms');
}, 3000)
setTimeout(function () {
console.log('我是第一段script中的宏任务2,延迟3000ms');
}, 3000)
setTimeout(function () {
console.log('我是第一段script中的宏任务3,延迟2000ms');
}, 2000)
</script>
<script>
setTimeout(function () {
console.log('我是第二段script中的宏任务1,延迟3000ms');
}, 3000)
setTimeout(function () {
console.log('我是第二段script中的宏任务2,延迟2000ms');
}, 2000)
</script>

1-7

具体来说:微任务与微任务之间根据先后顺序执行,宏任务与宏任务之间根据延迟时间顺序执行,如果延迟时间相同,则按照先后顺序执行,否则延迟时间小的先执行

那么,再次提出一个问题,第一段script执行完之后,我们遇到了下一个script这个大的宏任务,和第一段中的setTimeout这两个宏任务,也就是说script与setTImeout的执行顺序如何呢? 再来看一个测试

1
2
3
4
5
6
7
8
9
10
11
<script>
setTimeout(function () {
console.log('我是第一段script中的宏任务,延迟0ms');
}, 0)
</script>
<script>
console.log('我是第二段代码中的同步任务');
setTimeout(function () {
console.log('我是第二段script中的宏任务,延迟0ms');
}, 0)
</script>

根据测试结果,我们可以得出结论,

即便setTimeout的延迟设置为0,script也会先于setTimeout执行!

1-8

setTimeout为什么会超时?

1
2
3
4
5
6
7
setTimeout(() => {
console.log('三秒之后打印我');
}, 3000)

for(let i = 0;i<3000;i++){
console.log('打印三千次');
}

1-5

如图所示,因为setTimeout是一个异步任务,先等主程序的同步任务执行完才会执行,当主程序的同步任务执行超过了3s,setTimeout的执行就会滞后了

浏览器中的Event Loop事件循环机制

Event Loop是由JS的宿主浏览器来实现的

1-6

  1. 函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;
  2. 此期间WebAPIs完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
  3. 执行栈为空时,Event Loop把微任务队列执行清空;
  4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入Stack(栈)中执行,回到第1步。

关于浏览器的Event Loop事件机制的更深一步剖析,我将在下一篇文章中涉及,详见《从浏览器运行机制到Event Loop事件循环机制》


如果这篇文章对你有帮助,可以bilibili关注一波 ~ !此外,如果你觉得本人的文章侵犯了你的著作权,请联系我删除~谢谢!