js事件机制

感觉最近过的浑浑噩噩的,应该静下心来学习一些知识了,不管以后是做前端还是后端,静下心来学习总归是重要的。用胡适先生的话来鼓励自己:管什么真理无穷,进一寸有进一寸的欢喜。

调用栈与任务队列

在js是一门单线程语言,这里的单线程指的是在JS引擎中负责解释和执行js代码的线程只有一个,叫做main thread。

但是实际上还存在其他的线程。例如:处理AJAX请求的线程、处理DOM事件的线程、定时器线程、读写文件的线程(例如在Node.js中)等等。这些线程可能存在于JS引擎之内,也可能存在于JS引擎之外,在此我们不做区分。不妨叫它们工作线程。

在js的事件机制中很重要的两个概念是调用堆栈和任务队列,调用堆栈可以想象成同步任务,任务队列中保存的是已经满足条件需要被执行的异步任务的回调函数。对一个调用堆栈中的task进行处理的时候,其他都得等着。如果在执行过程中遇到setTimeout等异步操作的时候,会把setTimeOut交给浏览器的其他模块(以webkit为例,是webcore模块)进行处理,当到达setTimeout指定的延时执行的时间之后,task(回调函数)会放入到任务队列之中。一般不同的异步任务的回调函数会放入不同的任务队列之中。等到调用栈中所有task执行完毕之后,接着去执行任务队列之中的task(回调函数)。

image

在上图中,调用栈中遇到DOM操作、ajax请求以及setTimeout等WebAPIs的时候就会交给浏览器内核的其他模块进行处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。等到这些模块处理完这些操作的时候将回调函数放入任务队列中,之后等栈中的task执行完之后再去执行任务队列之中的回调函数。

setTimeOut与循环机制

  1. 首先将main函数的执行上下文入栈
    image
  2. 代码接着执行,遇到console.log(‘Hi’),此时log(‘Hi’)入栈,console.log方法只是一个webkit内核支持的普通的方法,所以log(‘Hi’)方法立即被执行。此时输出’Hi’。
    image
  3. 当遇到setTimeout的时候,执行引擎将其添加到栈中999。
    image
  4. 调用栈发现setTimeout是之前提到的WebAPIs中的API,因此将其出栈之后将延时执行的函数交给浏览器的timer模块进行处理。
    image
  5. timer模块去处理延时执行的函数,此时执行引擎接着执行将log(‘SJS’)添加到栈中,此时输出’SJS’
    image
  6. 当timer模块中延时方法规定的时间到了之后就将其放入到任务队列之中,此时调用栈中的task已经全部执行完毕。
    image
  7. 调用栈中的task执行完毕之后,执行引擎会接着看执行任务队列中是否有需要执行的回调函数。这里的cb函数被执行引擎添加到调用栈中,接着执行里面的代码,输出’there’。等到执行结束之后再出栈。

image
image

事件机制深入理解

接下来将介绍SetTimeOut,SetInterval,Promise,nextTick等函数在时间机制中的执行时机。

先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
})()

输出1,2,3,5,4 你做对了吗?

在这段代码里面,setTimeout和Promise都被称之为任务源,来自不同任务源的回调函数会被放进不同的任务队列里面。setTimeout的回调函数被放进setTimeout的任务队列之中。而对于Promise,它的回调函数并不是传进去的executer函数,而是其异步执行的then方法里面的参数,被放进Promise的任务队列之中。也就是说==Promise的第一个参数并不会被放进Promise的任务队列之中,而会在当前队列就执行,而将then方法里面的参数放进Promise的任务队列中。==

macro-task宏任务与micro-task微任务

  1. macro-task包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  2. micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver

事件循环的顺序是从script开始第一次循环,随后全局上下文进入函数调用栈,碰到macro-task就将其交给处理它的模块处理完之后将回调函数放进macro-task的队列之中,碰到micro-task也是将其回调函数放进micro-task的队列之中。直到函数调用栈清空只剩全局执行上下文,然后开始执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次执行macro-task中的一个任务队列,执行完之后再执行所有的micro-task,就这样一直循环。

总的来说

  1. 不同的任务会放进不同的任务队列之中。
  2. 先执行macro-task(这里指的是整体代码,非异步代码),等到函数调用栈清空之后再执行所有在队列之中的micro-task。
  3. 等到所有macro-task执行完之后再从micro-task中的一个任务队列开始执行,就这样一直循环。
  4. 当有多个macro-task(micro-task)队列时,事件循环的顺序是按上文macro-task(micro-task)的分类中书写的顺序执行的。

代码段的解释如下:

  1. script任务源先执行,全局上下文入栈。
    image
  2. script任务源的代码在执行时遇到setTimeout,作为一个macro-task,将其回调函数放入自己的队列之中。
    image
  3. script任务源的代码在执行时遇到Promise实例。Promise构造函数中的第一个参数是在当前任务直接执行不会被放入队列之中,因此此时输出 1 。
    image
    4.在for循环里面遇到resolve函数,函数入栈执行之后出栈,此时Promise的状态变成Fulfilled。代码接着执行遇到console.log(2),输出2。
    image
  4. 接着执行,代码遇到then方法,其回调函数作为micro-task入栈,进入Promise的任务队列之中。
    image
  5. 代码接着执行,此时遇到console.log(3),输出3。
    image
  6. 输出3之后第一个宏任务script的代码执行完毕,这时候开始开始执行所有在队列之中的micro-task。then的回调函数入栈执行完毕之后出栈,这时候输出5
    image
    image
  7. 这时候所有的micro-task执行完毕,第一轮循环结束。第二轮循环从setTimeout的任务队列开始,setTimeout的回调函数入栈执行完毕之后出栈,此时输出4。
    image )

终极例子

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
console.log('golb1');
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')
})
})
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')
})
setTimeout(function() {
console.log('timeout1_timeout1');
process.nextTick(function() {
console.log('timeout1_timeout1_nextTick');
})
setImmediate(function() {
console.log('timeout1_setImmediate1');
})
});
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
golb1
glob1_promise
glob1_nextTick
glob1_then
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
immediate1
immediate1_promise
immediate1_nextTick
immediate1_then
timeout1_timeout1
timeout1_timeout1_nextTick
timeout1_setImmediate1

总结:

  1. 同步代码执行顺序优先级高于异步代码执行顺序优先级;
  2. new Promise(fn)中的fn是同步执行;
  3. process.nextTick()>Promise.then()>setTimeout>setImmediate。

参考

  1. https://zhuanlan.zhihu.com/p/26229293
  2. https://zhuanlan.zhihu.com/p/26238030
  3. https://www.cnblogs.com/yonglin/p/7857804.html