1. 1. 前言
  2. 2. 一道面试题
  3. 3. 又一道面试题
  4. 4. 到底什么是 event loop?
  5. 5. 参考资料
  • 面试的时候被问到的一道题目
  • 什么是 Event Loop
  • setInterval() 执行函数的时间比间隔时间更长的效果,需要录屏软件放图片
  • 另外一个面试题目,关于 new Promise,setTimeout(fun, 0)。
  • reference

http://altitudelabs.com/blog/what-is-the-javascript-event-loop/
http://stackoverflow.com/questions/21607692/understanding-the-event-loop
https://developer.mozilla.org/en/docs/Web/JavaScript/EventLoop, not useful as others

1
2
3
4
5
6
7
8
9
10
11
console.log(1);
setTimeout(fn, 0);
console.log(2)
// 上面 line1, 2, 3 三个任务是串起来的,是一次要执行完的,中途不允许子线程的内容插进来。所以只有当这个 line1, 2, 3 穿起来的内容全都执行完毕之后才会允许 callback 的线程内容插入进来,开始执行他们的内容。setTimeou 本身就已经是一个动作了,这个动作就是说之后的某个时间段执行某个动作,这本身就是一个动作,这个动作结束之后,就会执行这个动作之后的动作,而不是执行这个异步函数。异步函数的优先级是低于这个动作所在的线程的。还有类似的
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
// 这里面的 on 表示一个挂载动作,挂在一个函数的动作,表示挂载 onload 动作,表示挂载 onerror 动作。这个动作具体是什么是不会现在去做的,而是把这个动作放入到了 子线程中。所以会一直执行到 send() 方法。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

再说直白一点,所有的 callback(回调) 都会被放入到子线程中,专业一点的说法是 tasks queue(任务队列) 中。记住这一点。一点遇到 callback 不用理会,直接继续向下执行。知道这个主线程中所有的内容都执行完了,再考虑 callback 的顺序问题。

one thread == one call stack == one thing at a time
one call stack 表示的就是这个 stack 结束之后才可以执行下一个 stack

用录屏的方式把视频中的一些示例效果录下来。https://vimeo.com/96425312
http://www.ruanyifeng.com/blog/2014/10/event-loop.html

  • 笔试的时候碰到了异步的题目,基本都答出来了。但是 new Promise().then() 的题目答错了。
  • 第二次 mt 面试的时候又被问到单线程的问题,setTimeout(fn, 0) 的问题,只是能说出来结果是什么,但是不知道什么意思。面试官建议我回去看看 event loop 相关的内容。
  • 回来就 google 了一下 event loop。找了几篇文章,还要视频看着就头大。
  • 去掘金上面搜了 event loop 关键词,出来一篇阮一峰的文章。打算先看看中文的解释。这篇文章里提到了一个视频,发现 google 搜出来的结果中也有这个视频。所以觉得这个视频应该是比较好的解释。然后仔细把这个二十多分钟的视频看了一遍。发现确实能学到很多内容。下面就把这些内容整理出来。
  • javascript runtime 是什么?memory allocation(heap) and call stack
  • call stack 是什么意思。就是执行完一个执行另一个。如果 call stack 中的某一个 call 有回调,产生回调的原因很多,但是主要就是图片中所罗列的三种情况,dom apis, setTimeou, ajax 等等。回调会放入到一个XXX 中,这个 XXX 会等待主线程,或者说 call stack 执行完毕之后再由 event loop 调用。
  • 我的几个小代码测试。比如 for () {console m, setTimeout(console n)}。还有就是可以解释 new Promise 的那个题目的解释。还有就是 setInterval(setTimeout )。都可以用录屏软件录制 gif 图进行展示。

前言

有些内容,值得看好多次,你才能真正理解其中的内涵,event loop 就是这样。很早之前看过 event loop 的内容,当时看的时候都能看懂,也以为自己理解了,但是实际真正再深入讨论的时候,会发现自己的理解还是不够深入。这两天花了点时间把 event loop 又重新深入的看了一下。对其中的内容已经理解了。把这些内容和大家分享出来。
先说下,对我最有帮助的就是下面这个视频,我也把这个视频中的很多内容都录屏做成了 gif 图片,供大家参考。

https://vimeo.com/96425312

在本文的最后我把所有参考的链接都贴出来了。除了这个视频之外,大家也可以看看其他内容,作为参考。

一道面试题

前几天去 xinmei 面试,在所有题目的最后考了 6 道题目,前 5 题全部是 for 遍历的和 setTimeout 结合的题目,考察最后输出什么。最后的题目是这样一道题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function(resolve) {
console.log(2);
for(var i = 0; i < 10000; i++) {
i == 9999 && resolve();
};
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
// 以上代码依次会输出什么?

这道题我不会,因为不明白 Promise, then 和 setTimeout 到底哪个会先执行?面试结束后面试官,告诉我了正确的答案,但是我还是没那么明白背后的原理。

通过在浏览器上打断点测试的方式你会发现 then 回调函数在瞬间完成,根本就没走 then 的函数,通过录屏的方式大家可以观察地更加清楚。
image
这段视频中,我首先在代码的第一行打了一个断点,然后执行整个代码,大家可以看到 setTimeout 函数作为整个 call stack 中的第一个函数,执行完毕之后立即去执行的就是 new Promise(),new Promise 中执行到 resolve 的时候,没有立即去执行 then,而是继续向后执行 console.log(3),resolve 作为异步函数,不会被 then 立即执行,因为 then 已经属于异步回调要做的事情了,then 不属于主线程中的 call stack 的一员,但是后面的 console.log(5) 属于首级调用栈中的内容个,所以 new Promise() 之后先打印 5。接着,戏剧性的一幕上演了,我点击下一步的时候,光标直接跳转到了 setTimeout 的位置,但是在控制台却立即打印出了 4 和 undefined,也就是说光标都没有跳转到 then 中的函数就直接把函数执行了,所以才会打印出 4。什么意思?当主线程,或者说首级调用栈结束之后,then 中回调的执行是立即的,是具有最高优先级的。甚至感觉走的都不是 tasks queue,这个不是很理解这其中的运行和设计机制,目前也没有找到正式的解释,如果大家有更合理的解释或者更官方正式的说法,可以在下面评论。所以说当 then 执行完毕之后才是开始 tasks queue中的回调队列,也就是 setTimeout(callback, 0) 中的 callback 去执行。这个时候才打印出最后的 1 。大家看不懂没关系,因为这个内容刚本来就是给你看完整篇文章之后,回头来看加深你对 event loop 理解的。而且这其中除了设计到 event loop 之外还牵扯到了 promise 的事情。就是让你在明白 event loop 运行机制的同时还能对 promise 有个更深的理解。

又一道面试题

无独有偶,上面的题目没有搞清楚,第二天又去 mt 面试。结果面试官依然问了我下面的题目:

1
2
3
4
5
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
console.log(3);

这个题目很简单,我也知道输出的结果。但是问题是面试官问我为什么会是这样的结果,你是否知道 event loop?说实话我我知道 setTimeout(fn, 0) 中,即使延迟参数为 0,也表示 fn 会异步执行,但是你让我深入去说什么是 event loop?以及这背后的运行原理和机制,其实我还是很难完整的说上来。好吧,我认了!吐槽一句,我自学前端快两年的时间,觉得自己进步还是蛮大的,但是很多面试官给我的评价都是学习速度很快,可惜我们现在的职位对于前端的要求比较高,所以你可能不太合适。每次听到这种话我就觉得想吐,呸~再见!
除了 event loop 之外,后续我还会对于 继承 和 闭包 也都会再做一个更加详细的说明和解释!因为他们都是需要多看几次的。

到底什么是 event loop?

和 event loop 相关的内容很多,或者说牵扯到的概念很多,或者说当说出这些内容的时候,我们首先想到的就应该是 event loop 这个运行机制:tasks queue, asychronous, block, thread, call stack, callback, setTimeout, event, ajax etc (任务队列,异步,阻塞 / 非阻塞,线程,回调,调用栈等等)。是不是感觉有点头大,看不下去了。talk is cheap, show me the code。那么 event loop 到底是个什么机制呢?我们先来看一段非常简单的代码。

1
2
3
4
5
6
7
function a() {
console.log('a');
};
console.log(1);
a();
console.log(2);

是不是很简单,依次打印出 1, a, 2。
我之所以把 function a 写到最上面,是因为实际上 JS 引擎也是这么做的,就是我们说的 hoisting 的一个过程。不懂的可以去查相关资料。
我们把上面这段代码打个断点看看到底是怎么执行的。
image
通过这个录屏我想告诉大家两个事情,一个是 call stack,一个是 scope,虽然本文主要想说的是 call stack,但是,scope 对于理解闭包有一定的帮助,大家知道这么一个事情就可以,后续我再写 闭包 的文章的时候,大家就可以更好的理解这个内容了。
上面的代码非常简单,我们在第 5 行打断点,然后执行整个代码。第 5 行从开始到结束,做了一件事情,就是打印 1。接着执行第 6 行,第 6 行要做的事情就是调用 a 函数。调用 a 函数的事情,就是去执行 a 函数的函数体,就是去执行 a 函数花括号中的内容,所以跳转到 console.log('a')console.log('a') 整行执行完毕就干了一件事情,打印 a,然后 a 函数执行完毕。执行第 7 行,同理,这行从开始到结束就是做了一件事情,打印 2。好像我说的很无聊,对不对?我们把上面的代码用图的形式表达出来看一下:

eventLoopExample3

我们上面讲了,当执行到 a() 的时候,其实就是去执行 function a 的函数体内部的内容,并且也是一行一行去执行。这里示意图的 … 表示就是函数体内部可能还会有其他的要执行内容,只不过,我们在这个例子中,为了简化内容而没有去写这些内容而已。所以第 6 行 执行 a() ,实际上是执行 a 函数内部的很多行内容。只不过通过函数的方式包裹起来,就简化成了一行的内容。上面的示意图,我觉得还是不够抽象,我们把整个执行过程再抽象一下。

eventLoopExample4

每一个点代表一个执行动作。
说这个有什么意义?我主要想说的是 cal stack 的概念。关于 call stack 的定义和内涵大家可以去看MDN 的这篇文章
我们再认真看看上面的录屏图片,当执行到 a 函数体的内部时,控制台右侧 call stack 展示了 a 和 anonymous 两个东西,anonymous 就是 test.js 本身,因为其实我们执行整段代码就是相当于把整段代码放到一个函数中去执行,只不过是一个 匿名函数 而已,所以就是 anonymous,而执行到 a 的时候,就会在 anonymous 上面再生成一个栈元素。当 a 执行完毕之后,这个栈元素会从栈中被推出,继续回到 anonymous 函数。这就是 call stack。也就是说整个过程是,执行函数,退出函数,回到 anonymous 函数的过程。a 是 anonymous 中的一个变量。当我们理解了 call stack 之后,我们再来看这段代码,再把这段代码稍加改动。

1
2
3
4
5
console.log(1);
setTimeout(function() {
console.log('a');
}, 0);
console.log(2);

我们把函数 a 改成了 setTimeout,其实 setTimeout 相当于是 window.setTimeout(),或者说 setTimeout 也是一个函数,只不过不是有我们自己来定义和声明的而已,是内置的。那么 setTimeout 做了一件什么事情呢?就是多少毫秒之后执行回调函数。这是一个事情,这是setTimeout 做的事情,这件事情做完,setTimeout 就运行结束了,就和 a 函数运行结束一样,开始执行函数后面的代码了。那如果说像我们上面所写的一样,我们给的延迟执行参数是 0 呢?那不就表示说是立即执行吗?那应该是在 console.log(2) 之前执行的啊。可是为什么不是这样的呢?我上面讲了,setTimeout 做的事情就是规定多少毫秒之后执行一个函数,这已经是一件事情了,这件事情做完 setTimeout 就从 call stack 中推出去了,回到 anonymous 了,然后继续执行 anonymous 中的下一行内容。而只有当 anonymous 所有的 call stack 都执行完毕之后,才开始看 回调函数 是否进入到待执行队列中,然后才执行。也就是说 setTimeout(callback, 0) 中的 callback 是永远也不会在 call stack 全部执行完毕之前插入执行的。这个 callback 走的是另外一个系统。我们可以用主线程来形容 anonymous 的进度,而 callback 走的是子线程,子线程必须要等到主线程空出来之后才会开始执行。子线程中的 callback 是永远也无法插入主线程的。我们再写一段代码,来看到底发生了什么。

1
2
3
4
5
setTimeout(function() {
console.log(1);
}, 0);
var ms = 3000 + new Date().getTime();
while(new Data().getTime() < ms) {};

我们把这段代码放入到浏览器中执行,看结果,你会发现尽管我们约定了 0 秒之后打印 1,但是因为 callback 是永远无法插入主线程的,所以在主线程的 while 将近 3 秒的执行时间内,callback 一直都没有执行!直到 3 秒只有才打印 1。

好,那既然这样 setTimeout 中的 callback 到底去哪里了呢?它有如何知道主线程已经执行完毕了呢?回调的形式是如何实现调用的?

The decoupling of the caller from the response allows for the JavaScript runtime to do other things while waiting for your asynchronous operation to complete and their callbacks to fire. But where in memory do these callbacks live – and in what order are they executed? What causes them to be called?

callback 被放置到内存的哪里了?他们会按照怎样的顺序调用?

我们需要来仔细看看 Philip Roberts 的视频。

eventLoopExample5

eventLoopExample6

eventLoopExample7
javascript 是 single thread,所以就意味着只能有一个 call stack。

多说一句,各种博客对于 任务队列 的称呼不一致,所以 message queue == callback queue == task queue。

通常来讲,异步相关的操作,主要是三类:一类是 setTimeout 产生的,一类是 ajax 产生的,一类是 click 等事件产生的。这三类情况都会有相应的 callback,所以说当有这三种情况的任何一种时,callback 都会被放入到 tasks queue 中,而主线程会一直进行下去,不会受到阻塞。只有当主线程执行完毕之后才会执行 callback。for 遍历是同步的,是属于主线程的,while 也是。

参考资料

通常参考资料就是罗列好几个链接。不过我觉得有必要明确一下每个链接的核心,以及链接之间的相互关系等等。
我先 google 了 event loop,第一个出现的就是 MDN 的 Concurrency model and Event Loop 这篇文章,这篇文章的内容比较短,感觉没有看的很明白。然后就是 Philip Roberts 的一个视频,然后就是一些 stack overflow 的问答,还有一些博客。当时打开博客感觉很难懂的样子。所以我就去 掘金 上搜了一下 event loop,第一篇相关的文章就是阮一峰写的再谈 Event Loop 这篇文章,然后这篇文章的开头就给出了一个视频的链接,就是 Philip Roberts 的演讲视频。我先把这个视频看了一遍,发现一下就全都懂了,视频解释的太清楚了,所以后面 阮一峰 的文章我就大概看了一下,没有再仔细看了。目前来看,我觉得这个视频的连接足够理解 event loop 了。所以下面的参考链接我按照重要的程度,分别打了分数,一个 + 表示 1 分,一个 - 号表示 0.5 分,总分是 5 分。
https://vimeo.com/96425312, +++++
http://2014.jsconf.eu/speakers/philip-roberts-what-the-heck-is-the-event-loop-anyway.html, ++++-, 这个视频和上一个视频基本是一样的,建议看第一个就行了。之所以还把这个贴出来是因为觉得这个网站不错,以后可以看看上面的其他内容。

http://www.ruanyifeng.com/blog/2014/10/event-loop.html, 这个就是阮一峰的总结,+++-。
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=0ahUKEwjr5rfsoojUAhVBw1QKHYGZAGEQFggjMAA&url=https%3A%2F%2Fdeveloper.mozilla.org%2Fen%2Fdocs%2FWeb%2FJavaScript%2FEventLoop&usg=AFQjCNGg94JT54olScLnJlTb2mIE1_RQUA&sig2=2-BEu4RdA-GNkFcBHXSfug, 这个是 MDN 的内容,+++-
http://blog.carbonfive.com/2013/10/27/the-javascript-event-loop-explained,这篇博客写的也很完善。值得一读。
http://www.cnblogs.com/xianyulaodi/p/6414805.html