xyhthink

事件循环EventLoop

   

学习JavaScript的执行机制,例如:同步任务和异步任务,以及异步任务中的宏任务和微任务的执行次序。

经典面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function async1() {
console.log(1)
await async2()
console.log(2)
}
async function async2() {
console.log(3)
}
console.log(4)
setTimeout(function () {
console.log(5)
}, 0)
async1()
new Promise(function (resolve) {
console.log(6)
resolve()
}).then(function () {
console.log(7)
})
console.log(8)
// 请写出输出结果...

如果你的答案和在浏览器上运行的一样,那么恭喜你,你的基础知识很牢固,可以点右上角的叉叉了。不过我相信大多数人还是对此晕头转向,不知道到底上面的代码该怎么执行。此时,不要慌,因为看了本文,你就可以玩转这类面试题了。

关于JavaScript

JavaScript是一门单线程语言,即使在最新的HTML5中提出了Web-Worker,但JavaScript是单线程这一核心仍未改变。所以一切JavaScript版的”多线程”都是用单线程模拟出来的,一切JavaScript多线程都是纸老虎。

JavaScript事件循环

JS是单线程的,那就意味着任务要一个一个按照顺序执行。如果一个任务耗时过长,那么后面的任务也必须等待。于是聪明的程序员就将任务分为两类:

  • 同步任务
  • 异步任务

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

  • 同步任务和异步任务分别进入不同的队列,同步任务进入主线程,异步的进入Event Table并注册函数

  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue(事件队列)

  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行

  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)

    那么如何知道主线程执行栈是否为空呢?

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

​ 例如一段简易的ajax请求:

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

  2. 执行console.log('代码执行结束')

  3. ajax事件完成,回调函数success进入Event Queue

  4. 主线程从Event Queue读取回调函数success并执行。

setTimeout

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。

例如实现延时3秒执行:

1
2
3
setTimeout(() => {
console.log('延时3秒');
},3000)

但是setTimeout用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这是什么情况?

不妨再看一个例子:

1
2
3
4
5
setTimeout(() => {
task();
},3000)

sleep(30000);

按照我们预期的设想,三秒后应该执行task(),但是你执行后发现,竟然在半分钟之后才执行。 为什么呢?

这时候我们需要重新理解setTimeout的定义。

上面代码的步骤如下:

  1. task()进入Event Table并注册,计时开始
  2. 执行sleep函数,很慢,非常慢,计时仍在继续
  3. 3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等待
  4. sleep终于执行完了,task()终于从Event Queue进入了主线程执行。

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为task())加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

​ 我们还经常遇到setTimeout(fn,0)这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

答案是不会的,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。

举例说明:

1
2
3
4
setTimeout(() => {
console.log("执行啦");
}, 0);
console.log("先执行这里")

上面代码的执行结果为:

1
2
// 先执行这里
// 执行啦

所以,我们可以得出结论,setTimeout 即使设置延时0毫秒,也不会立即执行,只有等当前同步任务执行完之后,才去执行setTimeout的代码。而且即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。

setInterval

说完了setTimeout,当然不能错过它的孪生兄弟setInterval 。他俩差不多,只不过后者是循环的执行。对于执行顺序来说,setInterval会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是每过ms秒,会有fn进入Event Queue。一旦setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了

Promise与process.nextTick

传统的定时器我们已经研究过了,接着我们探究Promiseprocess.nextTick(callback)的表现。

Promise的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise

process.nextTick(callback)类似node.js版的”setTimeout”,在事件循环的下一次循环中调用 callback 回调函数。(不了解可以暂时略过)

进入正题:

除了广义的同步任务和异步任务,我们对任务有了更精细的定义

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick,async

不同类型的任务会进入对应的Event Queue,比如setTimeoutsetInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() {
console.log('setTimeout');
}, 0)

new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})

console.log('console');

分析代码:

  1. 这段代码作为宏任务,进入主线程
  2. 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
  3. 接下来遇到了Promisenew Promise立即执行,then函数分发到微任务Event Queue
  4. 遇到console.log(),立即执行
  5. 整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行
  6. 第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行
  7. 宏任务Event Queue为空,结束。

事件循环,宏任务,微任务的关系如图所示:

我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

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
console.log('1');

setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})

第一轮事件循环流程分析如下:

  1. 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  2. 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
  3. 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1
  4. 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1
  5. 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2

此时,宏任务和微任务队列中的任务如下:

宏任务Event Queue 微任务Event Queue
setTimeout1 process1
setTimeout2 then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

我们发现了process1then1两个微任务

  • 执行process1,输出6
  • 执行then1,输出8

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

那么第二轮时间循环从setTimeout1宏任务开始:

  1. 遇到console.log,直接输出2
  2. 接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2
  3. new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

此时,宏任务和微任务队列中的任务如下:

宏任务Event Queue 微任务Event Queue
setTimeout2 process2
then2

上表是第二轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了2和4。

我们发现了process2then2两个微任务

  • 执行process2,输出3
  • 执行then2,输出5

第二轮事件循环结束,第二轮输出结果是2,4,3,5。

第三轮事件循环开始,此时只剩setTimeout2了,执行。

  1. 遇到console.log,直接输出9
  2. 接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process3
  3. 直接执行new Promise,输出11,then也分发到微任务Event Queue中,记为then3

此时,宏任务和微任务队列中的任务如下:

宏任务Event Queue 微任务Event Queue
process3
then3

上表是第三轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了9和11。

我们发现了process3then3两个微任务

  • 执行process3,输出10
  • 执行then3,输出12

第三轮事件循环结束,第三轮输出结果是9,11,10,12。

此时宏任务和微任务的Event Queue队列均为空,结束。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

总结

js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

相信你看到这里,再返回来看开篇的面试题,就觉得易如反掌了吧?

参考文献

  • 这一次,彻底弄懂JavaScript执行机制ssssyoki

  • ECMAScript 6 入门阮一峰

  • JavaScript 教程

 评论


Power by Yuhangxie , 总访问量为 次 。
载入天数...载入时分秒...
京ICP备19024986号