在切入正题之前呢,先说点题外话,最近准备面试,边投简历边刷面经,在刷面经的过程中 遇到频率很高的题目就是事件循环、闭包、原型链等等,当我们听到这些问题的时候,我感觉有90%多的人似懂非懂,也不知道该怎么描述,题主也是一顿百度,开始以为自己理解了,但是前几天的一道笔试题,有一次搞蒙了我,而百度上的文章呢讲的又是浅尝辄止,画几个图,几段简单的代码,会让读者拥有掌握的心里,其实并没有完全掌握,题主也是花费了很长事件才参透其中奥妙,下面给大家一步步讲解,最后,本文是我第一篇技术文章,所以可能会有很多需要改进的地方希望各位能谅解,也欢迎各位同胞提出建议,不啰嗦直接进入主题。
读前准备
在阅读之前,题主建议先去了解如下知识点,如果已经了解可以忽略。
- 函数调用栈(call stack)
- 队列数据结构(queue)
- Promise
正题
好的,我们就先抛出概念,然后再配合代码演示。
- javascript是单线程的语言,也就是说,同一个时间只能做一件事。
- 对于同步代码执行是阻塞的,也就是说上一段代码未执行结束之前,当前代码是无法执行的,而对于异步代码,js引擎遇到后不会一直等待其返回结果,而是会将其挂起,其返回结果后会进入任务队列等待主线程处理。
- 任务队列是和异步任务相关的队列,它是队列这种先入先出的数据结构,和排队是类似的,哪个异步操作完成的早,就排在前面。
- 事件循环是调控同步和异步任务的。
- 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个(JS只有俩个一个宏任务队列,一个微任务队列)。
- 任务队列又分为macro-task(宏任务)与micro-task(微任务)。
- macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
- micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
- 事件循环的顺序,决定了JavaScript代码的执行顺序。
- 它从script(整体代码)开始第一次循环。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从宏任务(macro-task)中取出一个放入调用栈执行,执行完毕后再执行所有的微任务(micro-task),所有微任务执行完毕后再从宏任务(macro-task)中取出一个执行......依次循环,直到所有任务执行完毕。
- 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
文字性的东西已经介绍完毕,下面带俩个例子帮助大家理解
-
//次例子引用网上常见的一个例子 setTimeout(function() { console.log('1'); }) new Promise(function(resolve) { console.log('2'); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log('3'); }).then(function() { console.log('4'); }) console.log('5');
我们来一步一步分析:
首先,初始化如下状态,调用栈里面只有一个全局环境。
然后遇到setTimeout,setTimeout属于宏任务源,所以将其挂起,返回结果后加入宏任务队列。(setTimeout也是一个函数,所以本身是同步的,但是执行其第一个参数是则是异步的,第一个参数也就是一个回掉函数,也就是说加入任务队列的不是setTimeout本身,而是function(){console.log(1);}) 继续执行代码,当遇到Promise后直接执行其构造函数,控制台打印2,resolve后将其then方法放入微任务队列,然后控制台打印3。 继续向下执行打印5,此刻执行栈内已经全部执行完毕,开始检查微任务队列,发现then,将其加入执行栈执行,打印4,这里只有一个任务,如果微任务队列还有其他任务会一直执行,直到清空微任务队列。 微任务队列执行完毕后,开始检查宏任务队列,发现setTimeout,将其放入执行栈执行,打印1,执行完毕后检查微任务队列,微任务队列为空,在检查宏任务队列,宏任务队列也为空,执行完毕! -
上面是一个比较简单的示例,当时题主在百度里面见到的也都是这样难度的示例,看完之后也是理解了,感觉很简单,但是上面的示例还不能体现出你掌握了整个执行流程,下面是我自己写的一个示例,大家可以先不看下面分析,可以先自己分析一遍输出情况,代码如下:
console.log('process start!') setTimeout(function () { //s1 console.log(1); setTimeout(function () { //s2 console.log(100); }, 1000); Promise.resolve(11) //p3 .then(res => { console.log(res); }) }, 0); setTimeout(function () { //s3 console.log(2) setTimeout(function () { //s4 console.log(200); }, 500); Promise.resolve(12) //p4 .then(res => { console.log(res); }) }, 1000); var p1 = new Promise((resolve) => { setTimeout(function () { //s5 resolve(5) console.log(3); }, 2000); console.log(8); let p2 = new Promise(resolve => { setTimeout(function () { //s6 console.log(7); }, 1000) console.log(9); resolve(4); }) resolve(6); p2.then(res => { console.log(res); }) }) p1.then(res => { console.log(res); }) console.log('process end!')
看完上面的代码是不是感觉有点吓人,心里想项目里哪有这样写代码的,哈哈,可能没有,这个代码只是帮助我们理解,虽然代码很长,但是我们一步一步分析。 首先,初始化如下状态,调用栈里面只有一个全局环境,和上面一样哈 ^_^
第一行遇到console打印process start!,继续向下执行遇到setTimeout,返回结果后(这里再次赘述一遍,返回结果可以理解为setTimeout里面第一个参数,也就是回掉函数)将其放入宏任务队列等待执行。 遇到setTimeout s3,返回结果后将其放入宏任务队列等待执行。 继续向下,遇到Promise,立即执行其构造函数,遇到setTimeout,返回结果后将其放入宏任务队列,因为s5等待时间是最长的,所以它也是最后一个放入任务队列,题主设置在了这个位置,当然各位也不要钻牛角尖,我们知道它是最后放入的就好,当然下面图可能一下看不懂,看不懂可以先看下面的,下面看完就懂了,如下图。 遇到console,立即在控制台打印8,遇到Promise立即执行其构造函数,遇到setTimeout s6,返回结果后放入宏任务队列。 遇到console打印9,遇到p2的resolve,将p2示例的then方法放入微任务队列。 遇到p1的resolve函数,将p1的then方法放入微任务队列,遇到console立即打印process end!。 到这里执行栈已经执行完毕,开始检查微任务队列,首先执行p2的then方法,将其加入执行栈,执行完毕后打印4,p2 then出栈。 接着执行微任务队列中p2的then方法,出队加入执行栈,执行后打印6,p1出栈。 微任务队列检查为空后,检查宏任务队列,取出s1执行,遇到console打印1,遇到setTimeout,将s2挂起,返回结果后加入任务队列。 遇到Promise p3立即执行其构造函数,遇到resolve方法后将其then方法加入微任务队列,p3执行完毕出栈。 s1函数执行完毕,出栈,执行栈为空,检查微任务队列,发现p3的then方法,将其放入执行栈执行,打印11,p3 then出栈,微任务队列为空。 检查宏任务队列,取出s3加入执行栈执行,打印2,遇到setTimeout将s4挂起,返回结果后加入任务队列。 遇到Promise p4立即执行其构造函数,遇到resolve方法后将其then方法加入微任务队列,Promise p4执行完毕出栈。 函数执行完毕,s3出栈,执行栈为空,检查微任务队列,发现p4的then方法,将其放入执行栈执行,打印12,微任务队列为空,p4 then出栈。 继续检查宏任务队列,发现s6,将其加入执行栈执行,打印7,执行完毕出栈。 执行栈为空检查微任务队列,微任务队列为空检查宏任务队列,发现s2,加入执行栈打印100,,执行完毕出栈。 执行栈为空检查微任务队列,微任务队列为空检查宏任务队列,发现s4,加入执行栈打印200,执行完毕出栈。 执行栈为空检查微任务队列,微任务队列为空检查宏任务队列,发现s5,加入执行栈打印3,执行完毕出栈,此时微任务队列宏任务队列都为空执行完毕!
到这里所有的过程就分析完毕了,聪明的的应该发现其中的机制来吧^_^,注意上文中提到“返回结果后....”,这里setTimeout不是说一上来就放入任务队列,比如setTimeout(fn,2000),是2000m后才把待执行任务fn放入任务队列,等待主线程执行,最后提一点关于node的事件循环,由上述代码中不难看出,不管是宏任务还是微任务都只有一个队列,但是node不同,其宏任务队列和微任务可以多个,如一个队列是setTimeout类型的,一个队列是setImmediate类型的。如果大家还想了解node的事件循环可以私下去了解这里不做解释。
文章到这里就结束了,第一次写可能存在很多“bug”,望大家谅解,也希望大家能提出宝贵的意见,也接受任何批评,如果感觉写的不错,那就给题主点个赞,你的赞将会是对题主最大的肯定。