JS的宏任务和微任务

执行机制

注意: 主线程中的代码执行也是宏任务,在主线程代码执行结束后,会先去检查也没有微任务,如果有先执行微任务,然后再去查看事件队列中还有没有需要执行的宏任务。

宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval
微任务的优先级: process.nextTick > Promise > MutationObserver

如果在一个微任务中递归新增微任务,是可以造成类似死循环的效果

async/await函数

因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似,

async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调

浏览器中的表现

在上边简单的说明了两种任务的差别,以及Event Loop的作用,那么在真实的浏览器中是什么表现呢?

假设有这样的一些DOM结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="renderer" content="webkit">
<title>Document</title>
<style>
#outer {
padding: 20px;
background: #616161;
}

#inner {
width: 100px;
height: 100px;
background: #757575;
}
</style>
</head>
<body>

<div id="outer">
<div id="inner"></div>
</div>
<script>
const $inner = document.querySelector("#inner");
const $outer = document.querySelector("#outer");

function handler() {
console.log("click"); // 直接输出

Promise.resolve().then(_ => console.log("promise")); // 注册微任务

setTimeout(_ => console.log("timeout")); // 注册宏任务

requestAnimationFrame(_ => console.log("animationFrame")); // 注册宏任务

$outer.setAttribute("data-random", Math.random()); // DOM属性修改,触发微任务
}

// 注册微任务
new MutationObserver(_ => {
console.log("observer");
}).observe($outer, {
attributes: true
});

$inner.addEventListener("click", handler);
$outer.addEventListener("click", handler);
</script>
</body>
</html>

如果点击#inner,其执行顺序一定是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout。

因为 click 触发了一个宏任务 handler,按照代码中的注释,在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了 Promise 和 MutationObserver 两个微任务,遂执行之。

因为click事件会冒泡,所以同时也触发了 #outer 的 click 事件,再次执行了 handler 函数,这个是一个同步的过程,所以会优先执行冒泡的事件(早于其他的宏任务),重复 #inner 的结果。

在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务,然后执行了 animationFrame 和 timeout。
需要注意的一点是,因为我们触发了 setAttribute ,实际上修改了 DOM 的属性,这会导致页面的重绘,而这个 setAttribute 的操作是同步执行的,也就是说requestAnimationFrame的回调会早于setTimeout所执行。

在Node中的表现

Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同,就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate。

setImmediate与setTimeout的区别

在官方文档中的定义,setImmediate 为一次 Event Loop 执行完毕后调用。setTimeout 则是通过计算一个延迟时间后进行执行。

但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。因为如果主进程中先注册了两个任务,然后执行的代码耗时超过setTimeout 的延迟时间,而这时定时器已经处于可执行回调的状态了。所以会先执行定时器,而执行完定时器以后才是结束了一次 Event Loop ,这时才会执行 setImmediate 。

1
2
setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

有兴趣的可以自己试验一下,执行多次真的会得到不同的结果。

但是如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:

1
2
3
4
5
6
7
8
setTimeout(_ => console.log("setTimeout"));
setImmediate(_ => console.log("setImmediate"));

let countdown = 1e9;

// 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`
while (countdown--) {
}

如果在另一个宏任务中,必然是setImmediate先执行:

1
2
3
4
5
6
require('fs').readFile(__dirname, _ => {
setTimeout(_ => console.log('timeout'))
setImmediate(_ => console.log('immediate'))
})

// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果

process.nextTick

这个可以认为是一个类似于 Promise 和 MutationObserver 的微任务实现,在代码执行的过程中可以随时插入 nextTick ,并且会保证在下一个宏任务开始之前所执行。

在使用方面的一个最常见的例子就是一些事件绑定类的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Lib extends require("events").EventEmitter {
constructor() {
super();

this.emit("init");
}
}

const lib = new Lib();

lib.on("init", _ => {
// 这里将永远不会执行
console.log("init!");
});

因为上述的代码在实例化Lib对象时是同步执行的,在实例化完成以后就立马发送了init事件。而这时在外层的主程序还没有开始执行到lib.on(‘init’)监听事件的这一步。所以会导致发送事件时没有回调,回调注册后事件不会再次发送。

我们可以很轻松的使用process.nextTick来解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Lib extends require("events").EventEmitter {
constructor() {
super();

process.nextTick(_ => {
this.emit("init");
});

// 同理使用其他的微任务
// 比如Promise.resolve().then(_ => this.emit('init'))
// 也可以实现相同的效果
}
}

这样会在主进程的代码执行完毕后,程序空闲时触发Event Loop流程查找有没有微任务,然后再发送init事件。
递归调用process.nextTick会导致报警,后续的代码永远不会被执行,这是对的,

参考

https://mbd.baidu.com/newspage/data/landingsuper?context=%7B%22nid%22%3A%22news_9060940668586593086%22%7D&n_type=1&p_from=3

https://www.cnblogs.com/jiasm/p/9482443.html