在网上看JS面试题的时候,经常会遇到以下这个题目,会问这段代码的执行结果:
1 | console.log(1) |
当然网上肯定也会有解答,但大多数都分析的不够彻底。现在就来彻底分析以下:
其实如果理解了在浏览器端JS的执行机制,就能很轻松的答对这道题目,而且再遇到类似的问题,也都不会害怕。
1、首先要记住一个核心概念:javascript是单线程语言,所谓单线程就是事情要一个一个顺序执行,如果前一个任务执行时间过长,后一个任务只能等待前一个任务执行完毕后,才可以执行
举例说一下:夜间去火车站买票,因为客流量少,只有一个售票窗口,大家都在排队买票,第一位乘客直接说要一张6点北京到上海的硬座车票,售票员很快就能将票打印出,乘客拿着票开心走了;第二位乘客说要一张7点北京到广州的卧铺,售票员需要问要哪个位置的,乘客说要下铺,售票员知道后打印了车票,这就比第一位乘客花的时间多了;第三位乘客说要一张北京到西安的车票,售票员需要问时间、座位类型、卧铺位置等等的,明确之后才能打印车票,这花费的时间就更多了。后面的乘客必须要等到前面的乘客买完后才能去买,即使前一位花了好长时间。
2、javascript的事件循环
浏览器如果都按着刚才例子的方式执行,当用户浏览到有图片的网站时,要等到图片加载完毕才能进行其它操作,就会损失很多用户。为了解决这个问题,就有了任务分类:
- 同步任务
- 异步任务
打开网站的时候,网页的渲染过程就是同步任务,而ajax数据获取、图片、视频等资源大加载慢的任务,就是异步任务。它们在执行的时候会有区别,分为以下几个过程:
- 同步任务进入到主线程中,马上执行
- 异步任务首先进入到事件表(Event Table)中,并注册回调函数
- 当异步的事件执行完毕后,Event Table会将其回调函数移送到事件队列中(Event Queue),等待执行
- 当主线程中的任务都执行完毕为空的时候,就会去Event Queue中读取对应的回调函数,并放入到主线程中执行
- 不断重复以上的过程,也就是事件循环(Event Loop)
注意点:setTimeout、setInterval平时都会说是过多长时间后执行,其实内在的原理是,遇到它们的时候,它们会进入事件表Event Table中,等过多长时间后,再将其回调函数移送到事件队列Event Queue中。还有就是,即使将时间设置为0,也不会存在完全的0ms,js最低时间为4ms
扩展:js引擎存在monitoring process进程,会持续检查主线程是否为空,如果为空,会马上去Event Queue中检查是否有等待被调用的函数
理解这两个概念之后,相信可以明白网上说的对题目的这段解释:
先输出1、3、4、6,因为这些任务都是同步执行的(new Promise是立即执行的,也是同步任务)
执行完毕之后再执行异步任务
但setTimeout和promise中的then都是异步执行的,应该先输出哪个呢?
3、除了广义的同步任务与异步任务外,这里又引入了对任务更精细的定义:
- macro-task(宏任务):包括script、setTimeout、setInterval等
- micro-task(微任务):包括Promise、ajax等
这两个不同的任务,会进入到不同的Event Queue,比如setTimeout和setInterval会进入到相同的Event Queue,但setTimeout和Promise则会进入到不同的Event Queue
js的事件循环顺序是,每一次循环都是先执行宏任务,然后再执行微任务。即第一次执行完所有的宏任务,接着执行所有的微任务,第一次循环结束;第二次循环依然是先执行完所有的宏任务,接着执行所有的微任务。但是还有一个注意点,如果是宏任务,会新建一个任务队列,任务队列中的宏任务有多个来源;如果是微任务,则直接放入微任务队列。
所以,事件循环可以归纳为以下的步骤:
- 全局任务
script
属于宏任务,所以最先执行,也就相当执行所有的同步任务,执行完之后,开始执行微任务 - 微任务队列中的任务都执行完后,读取宏任务队列中拍在最前面的宏任务
- 执行宏任务过程中,遇到微任务,会将其加入到微任务队列
- 执行完宏任务之后,继续读取微任务执行。依此类推
现在就可以完整的理解这道面试题了,仔细分析一下:
- 整体代码都在
script
标签之内包裹着,作为宏任务进入到主线程中 - 遇到
console.log(1)
的时候,马上执行输出1 - 遇到
setTimeout
的时候,会将其回调函数注册,过4ms后移送到宏任务Event Queue中 - 遇到
Promise
,由于new Promise
是立即执行的,这就输出了3、4, Promise
属于微任务,所以把then
函数放到微任务Event Queue中- 遇到
console.log(6)
,又会马上执行,输出6 - 此时第一次循环的宏任务全部执行完毕,开始执行微任务,在微任务Event Queue中发现了
console.log(5)
,输出5 - 由于第一次循环的宏任务与微任务全部执行完毕,开始进入第二次循环
- 第二次循环还是先执行宏任务,在宏任务Event Queue中发现了
console.log(2)
,输出2 - 现在已经没有其它任务了,整执行结束,所以最终结果是1、3、4、6、5、2
1、明白了javascript的执行机制,是不是做出这道题来就很简单了。再来看一道经典的题目:
1 |
|
其实和之前的题目很类似,只不过用了Promise.resolve方法,用过的同学应该知道,这是new Promise中resolve的语法糖。所以这道题和上一道题的分析过程一样,注意一点就是,题目中有两个then方法,这两个都要放到微任务Event Queue中。而在当前的循环中,不管是宏任务还是微任务,都是要执行Queue中的所有任务。记住了就能知道结果:
1 |
|
明白了javascript的执行机制,再遇到相似的题目,只要按着以上的逻辑逐行代码分析,就能很轻松的得到正确的答案。
2、接下来看一道宏微任务相互结合的题目:
1 | console.log(1) |
这道题里,别的地方和之前的题目没有什么差别,主要不同的地方是在第一个setTimeout
,在其内部又有一个promise
的then
方法,所以这是宏任务与微任务结合的题目,记住上面加粗的那句话以及事件循环执行顺序,就可以判断出,当执行第一个setTimeout
时,里面又有微任务,将其放入微任务队列,执行完这个宏任务,就会开始去读取微任务队列并执行,而不会先执行第二个setTimeout
,因为这个宏任务会新建任务队列,所以要等微任务执行完之后,再开始执行第二个setTimeout
,所以结果为1 6 5 2 3 4
如果把第一个setTimeout
的等待时间改为2000,那结果则是1 6 5 4 2 3,别忘了是先取排在前面的宏任务
3、最后再看一道题目:
1 | console.log(1) |
分析:
- 所有代码都在
script
标签中,整体当做宏任务,移送到主线程中 - 遇到
console.log(1)
,先输出1 - 遇到
setTimeout
,会将其回调函数注册,过100ms后移送到宏任务Event Queue中 - 遇到
Promise
,立即执行new Promise
里的任务,输出3 Promise
的then
放到微任务Event Queue中- 遇到微任务
ajax
,放到微任务Event Queue中 - 遇到
console.log(7)
,输出7。此时第一轮的宏任务已经全部执行完毕,开始执行微任务 - 在微任务队列中,拿到
then
,先遇到console.log(4)
,输出4,又遇到setTimeout
,将其回调注册,过4ms后移送到宏任务Event Queue中 - 拿到到
ajax
,遇到console.log(6)
,输出6。到此为止,第一轮已经全部执行完毕,开始执行下一轮 - 还是先执行宏任务,在宏任务队列中,遇到两个
setTimeout
,由于then
中的setTimeout
会先推入Event Queue中,所以会先输出5,之后再输出2
注意点:then中的setTimeout是0ms(也就是4ms)后放入宏任务Event Queue中,但ajax是40ms后放入微任务Event Queue中,虽然setTimeout快,但是由于ajax是微任务,而每次事件循环都是要执行完微任务再开始下一轮,所以即使ajax慢,也要等到执行完,才会执行下一轮宏任务的setTimeout
最后的最后,一定要记住以下几点:
- javascript是单线程语言
- Event Loop是javascript的执行机制
- 分清楚宏任务与微任务,记住每次Event loop都是要执行当前所有的宏任务与微任务
- 上面加粗说明的事件循环执行过程
注意:以上只是针对在browser即浏览器中的js,而在node中的执行机制是不同的。