JavaScript事件循环
最近看到一道基础题,沉痛打脸,发现对于JS的事件循环机制理解不扎实,于是梳理下,巩固基础。
基础题
先上原题
1 | console.log('start'); |
正确答案
1 | start |
解析
- 宏任务打印
start
- 定时器settimeout进入EventTable并注册,计时开始
- 遇到promise直接执行executor,打印
children4
,遇到settimeout,进入EventTable,此时第一轮任务执行完毕 - 第一定时器先进入队列,取出任务执行,打印
children2
,promise.then加入微队列,执行打印children3
- 第二定时器开始执行,打印
children5
,promise.then加入微队列,打印children7
- 定时器到时见,添加到宏任务,取出任务,打印
children6
背后知识点
JS是单线程语言,事件循环是JS的执行机制,但其它语言与之有所不同,比如
Java本身就支持多线程
同步与异步任务分别进入不同的
执行场所
异步任务又可以分为宏任务和微任务
举个例子,银行柜台员为每个客户办理业务,即是宏任务,但是客户可能突然需要增加办理任何一个业务,那么这些业务就可以看作是微任务
每个宏任务内都维持了一个微任务堵截,为了让高优先级及任务及时执行。也即是每取出一个宏任务,执行完毕之后。检查当前宏任务是否有微任务可执行。
宏任务:setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
微任务:process.nextTick(Node环境), Promises, queueMicrotask, MutationObserver
setTimeout即使设置的是0,并非理解执行回调函数。只有主线程执行栈内的任务全部执行完成,栈为空才从Event Queue中顺序取出执行。
Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.
事件循环的组成由主线程和任务队列,执行方式就是主线程不停从任务队列一个一个取出任务进行执行
测试题
搞清楚后,来两道题测试下
题1
注意:nodejs下执行
1 | console.log('1'); |
正确答案
1 | 1 |
解析
宏任务执行打印
1
遇到定时器放入延迟队列
遇到promise.nextTick,创建微任务,暂不执行
Promise executor同步代码立即执行,打印
7
,.then创建微任务,暂不执行遇到定时器放入延迟队列
没有同步代码,开始依次执行微任务,打印
6
,8
任务栈清空了,定时器时间也已经到了,延迟队列任务放入消息队列,依此开始执行
打印
2
,4
,执行微任务,打印
3 5
打印
9
,11
执行微任务,打印
10
,12
题2
1 | console.log('script start'); |
正确答案
1 | script start |
解析
- 宏任务打印
script start
- 定时器放入event table,开始计时
- promise.then是微任务,暂时先不执行,等待该任务队列宏任务执行结束
- 宏任务打印
script end
- 宏任务执行完毕,查看微任务,依次打印
promise1
,promise1
- 微任务执行完毕,定时器到时,放入event queue,取出任务,开始执行,打印
setTimeout
题3
1 | function foo() { |
片段1不会存在堆栈溢出而片段2会。
解析
- setTimeout为宏任务,而promise为微任务,宏任务在单个循环周期中一次一个地推入堆栈,但微任务总是在执行后返回到事件循环之前清空。因此,如果以处理条目的速度向这个队列添加条目,那么永远在处理微任务,只有当微任务队列为空时,时间循环才会重新渲染页面。
非阻塞?
搞清楚了JS的事件循环,并发模型,有必要再提下非阻塞。谈起nodejs,就经常会提起异步非阻塞。
要知道,异步非阻塞实际上是两个概念,异步
,非阻塞
,正如上面所说JS是单线程的,同一时间只能做一件事,因为设计之初的考虑,所以JS被定成了单线程。在此设计基础上,为了提高响应能力,就注定了异步的存在价值。同时,异步保证了不需要等待某些方法的执行结果可以继续执行其它的操作,所以才说非阻塞。
什么叫阻塞呢?比如浏览器端我们写上alert,如果用户不点击确认,根本不能进行其它任何操作,这就叫阻塞。
注意,异步非阻塞是JS的特性,浏览器端JS实际上也有。
写在最后
文章粗糙,如有问题,敬请斧正。