js中的事件机制

由于js是单线程的,所以在遇到事件绑定、ajax请求的时候回造成阻塞,因此就产生了异步操作。可以让js的主线程持续工作。在这个过程中主要有三个东西,一是主线程、二是任务队列、三是回调函数。

主线程就是不断地从任务队列里面取出一个事件去执行,在执行完这个时间之后再去任务队列取事件。如果任务队列没事件就会处于等待状态。

主线程取到任务队列里的事件之后会将事件的上下文放在调用栈中,然后一步一步执行里面的操作。如果遇到异步操作就会交给浏览器内核的其他模块进行处理,等处理完之后,再将回调函数放入任务队列汇总。

任务队列是用来存放事件的,当主线程完成当前操作之后,就会从这里面取事件执行。任务队列分为macro-taskmicro-task(micro-task只有一个)两种队列,主线程会先调用marco-task任务队列,直到函数调用栈中当前macro-task执行上下文结束,再执行micro-task中的任务,执行完micro-task中的事件后又去执行macro-task中的事件,如此反复。

macro-task:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。

micro-task: process.nextTick, Promises, Object.observe, MutationObserver

回调函数是在异步操作执行完之后,放入任务队列让主线程执行的函数。

示例:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// demo
console.log('golb1');

setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})

setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})

process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})

setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})

process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})

setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})

这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。

第一步:宏任务script首先执行。全局入栈。glob1输出。script首先执行

第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。 timeout1进入对应队列

第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。 进入setImmediate队列

第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。 nextTick

第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。 先是函数调用栈的变化)然后glob1_then任务进入队列

第六步:执行遇到第二个setTimeout。 timeout2进入对应队列

第七步:先后遇到nextTick与Promise glob2_nextTick与Promise任务分别进入各自的队列

第八步:再次遇到setImmediate。 nextTick

这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。

当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。

这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。第二轮循环初始状态

setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。

只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。

setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。

当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。