事件循环是学习JavaScript绕不过去的坎,也是面试过程中常出现的问题,弄清楚了事件循环可以更好的帮助我们理解JavaScript代码。
在理清事件循环之前,我们首先要理清一些基本的知识。
一,JavaScript为什么是单线程的?
关于这个问题,很多书籍上都有详细的解释,我在这里引用《ES6 标准入门》里面的说明来帮助我们理解。
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。 JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
在了解了JavaScript是单线程之后,可能又会产生另外的疑惑:JavaScript不是单线程的吗,为什么它可以执行异步操作呢?那样不就变成多线程了吗?
二,JavaScript是单线程的,为什么它可以执行异步操作?
通常我们说的JavaScript是单线程的,这个单线程指的是JS引擎线程,又称JS内核,它主要处理JavaScript脚本程序。
JS引擎线程是在浏览器中工作的,浏览器是支持多线程的 。浏览器中除了JS引擎线程 外,还主要有异步http请求线程 ,定时触发线程 ,事件处理线程 ,GUI渲染线程 等等。浏览器可以支持这些线程同时运行,但需要注意的是JS引擎线程和GUI渲染线程是互斥的,同一时间只能有一个线程处于工作中 。
因为JavaScript是单线程的,在浏览器中一个Tab上只能有一个JS引擎线程来执行JS脚本,当JS引擎遇到耗时比较久的任务时,比如说网络请求,JS引擎会将这个任务分配给异步http请求线程,由异步http请求线程执行网络请求任务,当获取到接口返回的结果后,再将结果传递给JS引擎线程。除了网络请求之外,还有定时器系列,像setTimeout,setIntervel之类,JS引擎线程会将这些任务分配给浏览器中定时触发线程。事件监听任务,JS引擎线程会将其分配给浏览器中事件处理线程处理。这样就实现了异步的效果。
JavaScript的单线程是指同一时间只能有一个JS引擎线程处于运行状态,JS脚本中的一些异步任务是交给浏览器中其他线程来完成的,浏览器是支持多线程同时运行的,这就是为什么JS是单线程,却可以执行异步操作。
三,同步任务与异步任务
之前说到过,JavaScript是单线程的,单线程就意味着所有的任务需要排队。就好比我们去超市买东西,买东西的人很多,但柜台只有一个收银员(单线程),所以我们在付款时需要排队。
但有时也会遇到一些特殊的情况,好比顾客在柜台付款时突然接到一个很急的电话,要等电话沟通结束后才能付款,但后面还有很多人在排队,这个时候一般都会把这个顾客摘出来放到旁边,让其继续电话沟通,后面排队的人继续付款,等这个顾客电话沟通结束后再进入排队付款。
同理,再排队的过程中,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种:同步任务和异步任务。
- 同步任务 :指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务 :指的是不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。好比之前说到打电话的顾客,将其放到旁边(任务队列),等打完电话后再排队付款(通知主线程,可以执行异步任务)。
四,异步任务执行过程
所有同步任务都在主线程上执行,形成一个执行栈。异步任务放在任务队列中。
需要注意的是,这里的异步任务和之前说的分配给浏览器其他线程的异步操作任务是有所区别的。这里任务队列中的任务指的是JS引擎线程分配给浏览器其他线程的任务执行完毕后的回调函数 。
通过一段简单代码来理解一下:
javascript
let a = 10;
let b = 20;
setTimeout(()=>{
console.log('两秒后输出:a+b=',a+b);
},2000);
console.log('立即输出:a+b=',a+b);
开始时,代码是在浏览器中JS引擎线程中的执行栈上执行,
当执行到setTimeout任务时,JS引擎线程会将这个任务丢给定时器触发线程 ,同时,代码继续向下执行 。
同一时间,定时器触发线程接收到setTimeout任务,并解析执行(不执行里面回调函数),在两秒后将回调函数放入任务队列 (task queue)中。这里有一个误区,包括我之前也陷入到里面。使用setTimeout设置的时间到达之后,回调函数是不立即执行的,而是定时器线程将其放入到任务队列中,等主线程执行完毕之后再执行。
最后,当执行栈中的JS代码执行完毕,JS引擎线程就会读取任务队列,依次执行任务队列中的回调函数。
将回调函数代码放入执行栈中执行,开启新一轮循环 。最后,等任务队列中的所有任务执行完毕,代码也就执行完毕。
需要注意的是,我们使用setTimeout设置的时间不一定准确,这是由主线程代码是否执行完决定的。
五,事件循环(Event Loop)
之前只是一段简单代码的执行过程,在实际的开发中,代码量会更大,产生的异步任务会更多。
JS引擎线程从任务队列中读取任务事件,这个过程是不断循环的。所以整个的这种运行机制又称为Event Loop(事件循环 )。
如图所示:
这里引用一下阮一峰老师对事件循环步骤的说明:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 (4)主线程不断重复上面的第三步。
六,宏任务,微任务
在ES6之前根本没有宏任务,微任务的划分,它们统称为任务(task),都属于异步任务 。ES6引入Promise之后,JavaScript引擎自身也能发起异步任务,为了和浏览器其他线程程的异步任务分开,也就有了宏任务和微任务的划分。
宏任务和微任务的区别
宏任务是由宿主(浏览器,node.js)发起的,微任务是由JavaScript自身发起的。
之前说到过,JavaScript引擎线程遇到耗时比较久的任务,如接口请求,定时器等等,会将这些任务丢给异步http请求线程,定时器触发线程等等。这些任务是浏览器发起的,就是属于宏任务 。像Promise这种JavaScript引擎可以自身发起的任务,不需要浏览器其他线程帮忙的,就是微任务。
宏任务和微任务的分类
宏任务主要有:
- JavaScript代码
- setTimeout、setInterval、setImmediate
- UI rendering、UI事件
- postMessage
- I/O
微任务主要有:
- Promise
- process.nextTick(Node.js)
- MutaionObserver
宏任务和微任务的执行时机
之前说过,宏任务和微任务都是属于异步任务,那它们在Event Loop中执行的顺序是什么呢?
在ES6引入Promise之后,任务队列就分成了两个子队列:宏任务队列和微任务队列。
执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务队列中所有 微任务执行完毕后,再将异步宏任务从队列中调入一个宏任务到主线程执行,执行完毕后再循环执行。
其示意图如下:主线程执行完毕,看是否有可执行的微任务。
!
参考文章:
JS的主线程及执行栈_js主线程-CSDN博客
什么是宏任务、微任务?宏任务、微任务有哪些?又是怎么执行的?-CSDN博客