Event Loop小韩详解!
声明: 本篇文章出自我们美丽可爱到爆棚的小韩小姐姐之手,如果有想要联系方式的记得发红包哈哈哈。感谢我们的小韩韩!
Event Loop(计算机系统的一种运行机制)
简单说一下系统的运行机制:计算机通常有一个或多个CPU及若干设备组成,通过公用总线相连。如下图所示,每个设备控制器都负责一类特定的设备。CPU和设备控制器既是并发执行的关系,也存在访问竞争。为了确保合理有序的访问共享内存,就必须通过内存控制器来协调访问内存数据。
程序运行的流程:
当计算机开始运行时,首先会执行一个初始程序。初始程序比较简单,多位于只读内存(ROM)中。这个程序会执行初始化操作,包括系统的各个组件,从CPU,寄存器,设备管理器到内存。为了执行此步骤,引导程序必须定位到操作系统内核,然后加载到内存中。
除了内核,系统程序也会提供一些服务。这些程序会成为系统进程,也就是我们常说的“后台程序”。其生命周期和内核一样。只要后台程序完成,整个系统就算完全启动,接下来就是等待其他事件的发生。
当有事件发生时,常常通过硬件或者软件的中断(interrupt)来通知用户。硬件通过系统总线发送相应的信号至CPU来触发终端操作;软件也可以执行系统调用(另称监督程序调用)来触发中断。
当CPU接到中断信号后,会停下正在运行的程序,然后立刻转到固定的位置再继续执行。一般来讲,固定位置含有中断程序的开始地址。在中断程序执行完毕后,CPU会重新执行被中断的程序。
总结:系统与设备控制器初始化——形成系统后台程序——等待用户事件,在收到新任务后执行中断程序中断当前操作——结束后返回。
JavaScript语言就采用这种机制,来解决单线程运行的问题~
那么就有了一个问题,单线程?为什么不是多线程?
JavaScript从诞生起就是单线程。原因大概是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。后来就约定俗成,JavaScript为一种单线程语言。
上图的绿色部分是程序的运行时间,红色部分是等待时间。这种运行方式称为”同步模式”。
如果采用多线程,同时运行多个任务,那很可能就是下面这样:
多线程不仅占用多倍的系统资源,也闲置多倍的资源,这显然不合理。
Event Loop就是为了解决这个问题提出来的。
它是一个程序的结构,用于等待和发送消息和事件的。
简单的说就是程序中设置两个线程:一个负责线程本身的运行,称为“主线程”;另一个是负责主线程和其他进程的。也就是各种 I/O操作 的通信;就被称为“Event Loop线程”
上图主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在红色的等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
可以看到,由于多出了橙色的空闲时间,所以主线程得以运行更多的任务,这就提高了效率。这种运行方式称为”异步模式“或”非堵塞模式”。
这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也因此使它具备了其他语言不具备的优势。
请看一个小故事:
以前有一个餐厅,这个餐厅有一个老板和一个厨师,自己创业的,刚开始起步阶段,没有资金请员工,所以自己来当老板兼服务员。
由于刚开业,所以会有一个充值优惠的活动,充了1000元是超级VIP客户,充了100元以上的是VIP客户,
所以来这家餐厅的顾客有这四种类型
1.家里有矿的超级VIP客户
2.充了钱的VIP客户
3.普通的客户
4.每次吃饭都带着一群人来吃的客户
作为VIP顾客,肯定得有VIP特权。
1.优先上菜
2.同等VIP,先点菜的先上菜
所以这个店的上菜顺序跟身份和点菜的顺序有关:
超级VIP客户 > VIP客户 > 普通客户 > 一桌的客户
这里为什么普通客户会大于一桌的客户呢?主要是因为炒一个人的菜比炒一桌人的菜快
一天、老板开始营业后,陆陆续续的来了一些人进来点餐吃饭。
1.第一个进来的是普通的客户,点了一道回锅肉
2.第二个进来的是充了钱的VIP客户,点了一道小龙虾
3.第三个进来的是一群人一起吃饭的客户,点了很多菜
4.第四个进来的是一个超级VIP客户,点了一道酸菜鱼
由于这个店只有一个人,所以老板招待好他们点完餐之后就去炒菜了。根据上面提到的顺序,所以会先炒超级VIP客户点的菜、然后到VIP客户点的菜、然后到普通客户点的菜、最后到一桌的客户点的菜。
接下来上代码:
我们定义了四个function:
- superVipOrder(name, dish) 用来表示超级VIP用户下单点菜
- vipOrder(name, dish) 用来表示VIP用户下单点菜
- order(name, dish) 用来表示普通用户下单点菜
- groupOrder(name, dish) 用来表示一桌子客户下单点菜
根据上面提到的上菜规则,
超级VIP客户 > VIP客户 > 普通客户 > 一桌的客户
实际的上菜顺序我们可以知道是
这些function都对应着JavaScript中的一些异步函数
我们可以暂且先把 process.nextTick 认为是超级vip用户,优先级最高、
原生Promise认为是vip用户,执行优先级高
setTimeout 认为是普通用户,执行优先级一般
setImmediate 认为是一群用户,执行优先级低
看异步函数:
根据上面故事提到的优先级规则,我们知道输出的结果是这样的:
我们知道 JavaScript是单线程的,就像上面故事的老板,他得去服务员去招待客人点菜,并将菜单给厨师,厨师炒好后再给到他去上菜。如果老板不请个厨师,自己来炒菜的话,那么在炒菜时就没办法接待客人,客人就会等待点菜。等着等着就会暴露出服务态度不行的问题。所以说,得有厨师专门处理炒菜的任务。
所以在js中,任务分为同步任务和异步任务
同步任务 -> 服务员去接待客人点菜
异步任务 -> 厨师炒菜、异步回调函数相当于 服务员去上菜
异步任务又分为: 宏任务 微任务
微任务:
原生的Promise -> 其实就是我们上面提到的VIP用户,
process.nextTick -> 其实就是我们上面提到的超级VIP用户,
process.nextTick的执行优先级高于Promise的
宏任务:
整体代码 script
setTimeout -> 其实就是我们上面提到的普通用户,
setImmediate -> 其实就是我们上面提到的群体用户,
setTimeout的执行优先级高于 setImmediate 的
宏任务与微任务的执行过程如图:
再来上代码:
执行结果为:b d e f a h c g
分析:
1. 首页执行第一个宏任务 整段script标签代码,遇到第一个 setTimeout,将其回调函数加入到宏任务队列中,输出 console.log(‘b’)
2. 遇到process.nextTick,将其回调函数加入到微任务
3. 遇到setImmediate 将其回调函数加入到宏任务队列中
4. 当第一个宏任务执行完后,就会去判断是否还有微任务,刚好有一个 微任务,执行process.nextTick的回调,输出 console.log(‘d’),然后又遇到了一个process.nextTick,又将其放入到微任务队列
5. 继续将微任务队列中的回调函数取出,继续执行,输出 console.log(‘e’),然后又遇到了一个process.nextTick,又将其放入到微任务队列
6. 继续将微任务队列中的回调函数取出,继续执行,输出 console.log(‘f’),然后又遇到了一个process.nextTick,又将其放入到微任务队列
7. 当微任务队列为空后,开始新的宏任务,取出第一个宏任务队列的函数,setTimeout,执行 console.log(‘a’),然后遇到Promise,process.nextTick 将其回调加入到微任务队列
8. 继续判断微任务队列是否有回调函数可执行,由于process.nextTick的执行优先级大于promise,所以会先执行process.nextTick的回调,输出 console.log(‘h’);、如果有多个process.nextTick的回调,会将process.nextTick的所有回调执行完成后才会去执行其它微任务的回调。
当nextTick所有的回调执行完后,执行promise的回调,输出console.log(‘c’);
9. 微任务执行完后,开始执行新的宏任务,执行setImmediate的回调,输出 console.log(‘g’);
是不是理解的差不多了,穿插一个小知识点: setImmediate
因为它属于宏任务的范畴,但又有点不一样的地方。
如果我们把使用Node 0.10.x的版本去执行这段代码,结果是输出a b c d
然而,在Node 大于 4.x 的版本后,在执行setImmediate的,会使用while循环,把所有的immediate回调取出来依次进行处理。
这也是我为什么把 setImmediate 比喻成 一桌子人客户的原因。
最后看一段代码看看自己是否真的掌握了
输出的结果为: start end a e g f h b d c i
分析:
1. 第一轮事件循环开始,执行script代码,输出 start end 将process.nextTick 的回调加入微任务队列中,将setImmediate的回调加入到宏任务的队列中
2. 执行微任务队列中的process.nextTick的回调,输出 a 、将setImmediate的回调加入到宏任务的队列中,遇到promise、将回调加入到微任务队列中。
3. 继续执行微任务队列中的回调,取出promise.then并执行,输出e,将process.nextTick的回调放入到微任务中,遇到promise、将回调加入到微任务队列中。
4. 判断当前promise的回调队列是否还有回调函数没执行,如果有,将继续执行,取出刚刚放入的promise的回调,当Promise回调队列执行完后,继续判断当前是否还有微任务。
5. 取出process.nextTick的回调并执行,输出g
6. 当前微任务队列为空后,开始执行宏任务,因为setTimeout的优先级大于setImmediate,所以先取出setTimeout的回调并执行,输出h
7. 当前微任务队列还是为空,开始执行宏任务,取出所有setImmediate的回调函数,并执行,输出b d 将 process.next 与 promise的回调放入到微任务队列中。
8. 取出微任务队列中的回调函数,并执行,输出 c i
Event Loop (事件循环)执行顺序总结:
- 在异步事件执行完操作后会放入一个执行队列里,而根据这个异步事件的类型会被放入对应的宏任务队列或者微任务队列中
- 当执行栈为空时,主线程会先去执行微任务队列中对应的回调函数,再去执行宏任务队列中的任务(当然是取出放入执行栈中执行)
- 在一次循环中,微任务永远在宏任务之前执行
故事到这里就结束了, 如果喜欢请收藏起来吧!