js宏任务和微任务(异步和同步执行的顺序)探究
前言
js属于单线程,也就是说对于一段js代码片段,只会顺序依次执行,都是同步执行的。而实际使用过程中有些任务执行时间长,有些任务执行时间短,这会导致页面加载时间过长并且出现空白页面的现象,非常影响用户体验。为了解决这个问题产生了宏任务和微任务。
一、什么是宏任务和微任务
广义上来说
宏任务包含:script(整体代码块)、setTimeOut、setInterval、setImmediate、I/O、UI rendering
微任务包含:promise、Object.observe、MutationObserver
也没有什么定义,只是将上面这些代码块分为宏任务和微任务。对于宏任务和微任务JS又做了不同的处理方式。
-
先从两张图片了解一下宏任务和微任务的处理方式。
这两张图片简要介绍了js任务执行流程以及宏任务和微任务执行流程。从这张图的理解来说:宏任务的优先级大于微任务的优先级,也就是说宏任务先执行,然后再执行微任务之后以此往复。 -
但是在有些视屏教程中介绍js的宏任务和微任务时,讲到微任务的优先级大于宏任务。
-
我的理解是:这两种说法都对,只是从看的起点角度不同罢了。对于第一种介绍来说。是将起始script代码块看做宏任务,这样子代码执行一开始就是宏任务。宏任务执行结束了再执行微任务,以此往复;对于第二种介绍。其实是拋过了起始script代码块,起始就是同步任务块,顺序执行。因此这样来说微任务的优先级就大于了宏任务。
-
哪个观点更好:我认为两个观点都好。第一种更严谨。从宏任务和微任务的分类来说。script整体代码块就是宏任务;但是对于学者来说,很容易忽略主线程scrpit也代表的是一个宏任务。所以第二种说法更易懂。
接下来从示例代码中更进一步了解宏任务和微任务的执行顺序。
二、图例解释
图例解释:js代码是依次同步执行的,如上图所示,当执行过程中遇到宏任务,宏任务不会立刻执行,先放在宏任务执行队列;遇到微任务,也不会立即执行,会放入微任务队列;只有遇到js的主线代码才会立即执行。当主线代码执行结束,会依次将微任务调用到主线js代码执行,微任务调用结束之后,再去宏任务队列依次调用宏任务到主线代码执行。直到宏任务执行结束。
三、示例代码
- 例1:
<script type="text/javascript">
setTimeout(function() {
console.log("宏任务1")
}, 0);
Promise.resolve()
.then(() => {
console.log("微任务1")
});
console.log("js主线代码1")
setTimeout(function() {
console.log("宏任务2")
}, 0);
Promise.resolve()
.then(() => {
console.log("微任务2")
});
console.log("js主线代码2")
</script>
结果:
- 例2:
<script type="text/javascript">
setTimeout(function() {
console.log("宏任务1")
}, 0);
new Promise(resolve => {
console.log("js主线任务3")
resolve();
})
.then(() => {
console.log("微任务1")
});
console.log("js主线代码1")
setTimeout(function() {
console.log("宏任务2")
}, 0);
new Promise(resolve => {
console.log("js主线任务4")
resolve();
})
.then(() => {
console.log("微任务2")
});
console.log("js主线代码2")
</script>
结果:
分析: 这两段代码的区别在与promise一个是new,一个直接调用方法。这里有一个误区:会以为new Promise对象的执行代码也放在微任务。事实上是,new对象是相当于主js线程立即执行的。只有then的代码表示异步(微任务)。这样也就解释了执行结果为什么是这样子的。
- 例3:
<script type="text/javascript">
setTimeout(function() {//代码段1
console.log("宏任务1")
Promise.resolve() //代码段2
.then(() => {
console.log("微任务2")
});
}, 0);
Promise.resolve() //代码段3
.then(() => {
console.log("微任务1")
setTimeout(function() { //代码段4
console.log("宏任务2")
}, 0);
});
console.log("js主线代码1") //代码段5
</script>
结果:
分析: 这段代码就有点复杂了。这一块就解释了图例中说的是一个层级的流程是这个样子的。 可结合文章第二张图片,理解执行顺序更简单。 先简单的分析执行流程:
- 首先执行遇到代码段1,宏任务,放入宏任务队列
- 遇到代码段3,微任务,放入微任务队列
- 遇到代码段5,非宏任务/微任务,执行结果,打印:js主线代码1
- 执行微任务队列,代码段3,打印:微任务1
- 遇到代码段4,宏任务,放入宏任务队列
- 此时这里的微任务执行结束,所以去宏任务队列去执行,执行打印:宏任务1
- 遇到代码段2,微任务,放入微任务队列
- 此时当前宏任务执行结束。去执行微任务,代码段2,打印:微任务2
- 微任务执行结束,再去执行宏任务,代码段4,打印:宏任务2
- 例4:
<script type="text/javascript">
setTimeout(function() {//代码段1
console.log("宏任务1")
Promise.resolve()//代码段2
.then(() => {
console.log("微任务2")
});
setTimeout(function() {//代码段3
console.log("宏任务3")
},0);
}, 0);
Promise.resolve()//代码段4
.then(() => {
console.log("微任务1")
setTimeout(function() {//代码段5
console.log("宏任务2")
}, 0);
Promise.resolve()//代码段6
.then(() => {
console.log("微任务3")
});
});
console.log("js主线代码1")
</script>
分析: 这个例子更复杂,可结合文章第二张图片,理解执行顺序更简单。 执行步骤如下:
- 遇到代码段1,宏任务,放入宏任务队列
- 遇到代码段4,微任务,放入微任务队列
- 打印:js主线代码1
- 执行所有微任务,打印:微任务1
- 遇到代码段5,宏任务,加入宏任务队列
- 遇到代码段6,微任务,加入微任务队列
- 重点:此时刚开始我以为会执行宏任务,但是实际上,是因为第6步又加入了一个微任务,此时微任务没有执行完,所以先执行微任务。即打印:微任务3
- 这是所有微任务执行完成。执行宏任务,即代码段1,打印:宏任务1
- 遇到代码段2,微任务,放入微任务队列
- 遇到代码段3,宏任务,放入宏任务队列
- 此时该宏任务执行完成,执行微任务,即代码段2,打印:微任务2
- 微任务执行结束,执行宏任务,即此时第一个宏任务就是代码段5,即打印:宏任务2
- 再去执行微任务,没有微任务执行宏任务,即代码段3,打印:宏任务3
- 至此,所有宏任务和微任务执行结束
总结
- 从定义上出发,宏任务优先执行与微任务。从理解角度出发,我认为微任务优先执行与宏任务这种说法比较好理解。
- 理解宏任务和微任务执行顺序的问题,可以结合文章第二张图片介绍的宏任务和微任务的执行流程(当然这图是网上搜的)
- 我的理解是,先执行同步代码,然后执行当前所有的微任务,然后执行一个宏任务,然后再执行所有的微任务。再执行一个宏任务。再执行所有的微任务。。。依次类推,执行结束。
- 当然执行宏任务和微任务内部同步代码也是先执行的
附:js定时器(setTimeOut)真的守时吗?
答案是否定的。定时器的执行不一定是等待一定时间后执行的。在图例解释中的图片可看出,setTimeOut是宏任务,当执行到setTimeOut时,先把setTimeOut交给定时器定时,当时间一到,放入宏任务队列去执行。 因此如果主线程执行时间过长,那么定时器是不会准时的。首先得等到主线程执行结束。如果主线程执行时间特别短,那么setTimeOut时间一到即立刻执行。
例如下代码块:
<script type="text/javascript">
setTimeout(function() {
console.log("定时器1")
}, 2000);
for (var i = 0; i < 1000000; i++) {
console.log(" ")
}
setTimeout(function() {
console.log("定时器2")
}, 3000);
</script>
这里执行结果是:当循环结束时立刻执行定时器1,然后执行定时器2。
这里也可以看出:js代码也不是说代码解析到哪里就开始执行到哪里,也许是先解析代码片段,然后依次解析到两个定时器,一个同步代码片段,将定时器放在定时器队列中,然后立刻执行同步片段,结束之后执行定时器。,如果说边解析边执行的话,那么会在循环结束时立刻执行定时器1,等待3秒之后执行定时器2,然而结果并非如此。