用大白话告诉你什么是Event Loop
文章
前沿
从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:
从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:
从前有座山,山里有座庙,庙里有个小和尚在讲故事、讲什么呢?讲的是:
...
请看一个小故事
以前有一个餐厅,这个餐厅有一个老板和一个厨师,自己创业的,刚开始起步阶段,没有资金请员工,所以自己来当老板兼服务员。
由于刚开业,所以会有一个充值优惠的活动,充了1000元是超级vip客户,充了100元以上的是vip客户,
所以来这家餐厅的顾客有这四种类型
- 家里有矿的超级vip客户
- 充了钱的vip客户
- 普通的客户
- 每次吃饭都带着一群人来吃的客户
作为vip顾客,肯定得有vip特权。
- 优先上菜
- 同等vip,先点菜的先上菜
所以这个店的上菜顺序跟身份和点菜的顺序有关,
超级vip客户 > vip客户 > 普通客户 > 一桌的客户
这里为什么普通客户会大于一桌的客户呢?主要是因为炒一个人的菜比炒一桌人的菜快
一天、老板开始营业后,陆陆续续的来了一些人进来点餐吃饭。
第一个进来的是普通的客户,点了一道回锅肉
第二个进来的是充了钱的vip客户,点了一道小龙虾
第三个进来的是一群人一起吃饭的客户,点了很多菜
第四个进来的是一个超级vip客户,点了一道酸菜鱼
由于这个店只有一个人,所以老板招待好他们点完餐之后就去炒菜了。根据上面提到的顺序,所以会先炒超级vip客户点的菜、然后到vip客户点的菜、然后到普通客户点的菜、最后到一桌的客户点的菜
让我们用伪代码看看如何实现这个逻辑
我们定义了四个function
- superviporder(name, dish) 用来表示超级vip用户下单点菜
- viporder(name, dish) 用来表示vip用户下单点菜
- order(name, dish) 用来表示普通用户下单点菜
- grouporder(name, dish) 用来表示一桌子客户下单点菜
根据上面提到的上菜规则,
超级vip客户 > vip客户 > 普通客户 > 一桌的客户
实际的上菜顺序我们可以知道是
那么问题来了,那些function都是什么呢?
其实很简单,这些function都对应着javascript中的一些异步函数
// 超级vip客户 // 微任务,将回调加入到 执行队列,优先执行 function superviporder(name, dish, order) { process.nexttick(() => { console.log(red(`supervip顾客 ${name} 点了 ${dish} 已经上了`)); }); }
// vip客户 // 微任务,将回调加入到 执行队列,优先执行,优先级比process.nexttick低 function viporder(name, dish, order) { new promise((resolve, reject) => { resolve(true); }).then(() => { console.log(blue(`vip顾客 ${name} 点了 ${dish} 已经上了`)); }); }
// 普通客户下单 // 宏任务,将回调加入到 宏任务的执行队列中 function order(name, dish, order) { settimeout(() => { console.log(yellow(`普通顾客 ${name} 点了 ${dish} 已经上了`)); }, 0); }
// 一桌的客户 function grouporder(name, dish, order) { setimmediate(() => { console.log(green(`一桌子顾客 ${name} 点了 ${dish} 已经上了`)); }); }
我们可以暂且先把 process.nexttick
认为是超级vip用户,优先级最高、
原生promise
认为是vip用户,执行优先级高
settimeout
认为是普通用户,执行优先级一般
setimmediate
认为是一群用户,执行优先级低
还原伪代码
我们将伪代码还原成这些异步函数,这会让我们看的更加直观、亲切一些
根据上面故事提到的优先级规则,我们知道输出的结果是这样的
为什么会是这样的结果呢?下面就来讲讲javascript中的event loop
event loop
1. javascript的事件循环
我们知道 javascript
是单线程的,就像上面故事的老板,他得去服务员去招待客人点菜,并将菜单给厨师,厨师炒好后再给到他去上菜。如果老板不请个厨师,自己来炒菜的话,那么在炒菜时就没办法接待客人,客人就会等待点菜。等着等着就会暴露出服务态度不行的问题。所以说,得有厨师专门处理炒菜的任务
所以在js中,任务分为同步任务和异步任务,
- 同步任务 -> 服务员去接待客人点菜
- 异步任务 -> 厨师炒菜、异步回调函数相当于 服务员去上菜
js的事件循环如图所示,
- 在执行主线程的任务时,如果有异步任务,会进入到event table并注册回调函数,当指定的事情完成后,会将这个回调函数放到 callback queue 中
- 在主线程执行完毕之后,会去读取 callback queue中的回调函数,进入主线程执行
- 不断的重复这个过程,也就是常说的event loop(事件循环)了
2. 异步任务
异步任务又分为宏任务跟微任务、他们之间的区别主要是执行顺序的不同。
在js中,微任务有
原生的promise -> 其实就是我们上面提到的vip用户,
process.nexttick -> 其实就是我们上面提到的超级vip用户,
process.nexttick的执行优先级高于promise的
宏任务
- 整体代码 script
- settimeout -> 其实就是我们上面提到的普通用户,
- setimmediate -> 其实就是我们上面提到的群体用户,
settimeout的执行优先级高于 setimmediate 的
宏任务与微任务的执行过程
在一次事件循环中,js会首先执行 整体代码 script,执行完后会去判断微任务队列中是否有微任务,如果有,将它们逐一执行完后在一次执行宏任务。如此流程
测试
下面我们来看一段代码是否了解了这个流程
<script> settimeout(() => { console.log('a'); new promise( res => { res() }).then( () => { console.log('c'); }) process.nexttick(() => { console.log('h'); }) }, 0) console.log('b'); process.nexttick( () => { console.log('d'); process.nexttick(() => { console.log('e'); process.nexttick(() => { console.log('f'); }) }) }) setimmediate( () => { console.log('g'); }) </script>
执行结果为:b d e f a h c g
让我们来分析一下这段代码的执行流程
首页执行第一个宏任务 整段
script
标签代码,遇到第一个settimeout
,将其回调函数加入到宏任务队列中,输出
console.log('b')
遇到process.nexttick,将其回调函数加入到微任务
遇到setimmediate 将其回调函数加入到宏任务队列中
宏任务event queue | 微任务event queue |
---|---|
settimeout | process.nexttick |
setimmediate |
- 当第一个宏任务执行完后,就会去判断是否还有微任务,刚好有一个 微任务,执行process.nexttick的回调,输出
console.log('d')
,然后又遇到了一个process.nexttick,又将其放入到微任务队列 - 继续将微任务队列中的回调函数取出,继续执行,输出
console.log('e')
,然后又遇到了一个process.nexttick,又将其放入到微任务队列 - 继续将微任务队列中的回调函数取出,继续执行,输出
console.log('f')
,然后又遇到了一个process.nexttick,又将其放入到微任务队列
宏任务event queue | 微任务event queue |
---|---|
settimeout | |
setimmediate |
- 当微任务队列为空后,开始新的宏任务,取出第一个宏任务队列的函数,
settimeout
,执行console.log('a')
,然后遇到promise
,process.nexttick
将其回调加入到微任务队列。执行完后
宏任务event queue | 微任务event queue |
---|---|
setimmediate | promise.then |
- | process.nexttick |
- 继续判断微任务队列是否有回调函数可执行,由于
process.nexttick
的执行优先级大于promise
,所以会先执行process.nexttick
的回调,输出console.log('h');
、如果有多个process.nexttick
的回调,会将process.nexttick
的所有回调执行完成后才会去执行其它微任务的回调。
当nexttick所有的回调执行完后,执行promise
的回调,输出console.log('c');
,直到promise的回调队列执行完后,又会去判断是否还有微任务。
宏任务event queue | 微任务event queue |
---|---|
setimmediate |
-
微任务执行完后,开始执行新的宏任务,执行
setimmediate
的回调,输出console.log('g');
### 3. setimmediate
这里为什么要把 setimmediate
单独拿出来说呢,因为它属于宏任务的范畴,但又有点不一样的地方。
先看一段代码
按照我们上面的分析逻辑,我们会认为这段代码的输出结果应该是a b c d
。
如果我们把使用node 0.10.x的版本去执行这段代码,结果确实是输出a b c d
然而,在node 大于 4.x 的版本后,在执行setimmediate
的,会使用while循环,把所有的immediate回调取出来依次进行处理。
这也是我为什么把 setimmediate
比喻成 一桌子人客户的原因。
## 最后看一段代码看看自己是否真的掌握了
如果还没有掌握,欢迎评论区吐槽
<script> console.log("start"); process.nexttick(() => { console.log("a"); setimmediate(() => { console.log("d"); }); new promise(res => res()).then(() => { console.log("e"); process.nexttick(() => { console.log("f"); }); new promise(r => { r() }) .then(() => { console.log("g"); }); settimeout(() => { console.log("h"); }); }); }); setimmediate(() => { console.log("b"); process.nexttick(() => { console.log("c"); }); new promise(res => res()).then(() => { console.log("i"); }); }); console.log("end"); </script>
输出的结果为: start end a e g f h b d c i
简单分析一下代码:
- 第一轮事件循环开始,执行
script
代码,输出start
end
,将process.nexttick
的回调加入微任务队列中,将setimmediate
的回调加入到宏任务的队列中 - 执行微任务队列中的
process.nexttick
的回调,输出a
、将setimmediate
的回调加入到宏任务的队列中,遇到promise
、将回调加入到微任务队列中。
宏任务 | 微任务 |
---|---|
setimmediate | promise.then |
setimmediate | - |
- 继续执行微任务队列中的回调,取出
promise.then
并执行,输出e
,将process.nexttick
的回调放入到微任务中,遇到promise
、将回调加入到微任务队列中。 - 判断当前promise的回调队列是否还有回调函数没执行,如果有,将继续执行,取出刚刚放入的promise的回调,输出
g
,当promise回调队列执行完后,继续判断当前是否还有微任务。 - 取出
process.nexttick
的回调并执行,输出g
宏任务 | 微任务 |
---|---|
setimmediate | - |
setimmediate | - |
settimeout | - |
- 当前微任务队列为空后,开始执行宏任务,因为
settimeout
的优先级大于setimmediate
,所以先取出settimeout
的回调并执行,输出h
- 当前微任务队列还是为空,开始执行宏任务,取出所有
setimmediate
的回调函数,并执行,输出b d
,将process.next
与promise
的回调放入到微任务队列中。 - 取出微任务队列中的回调函数,并执行,输出
c i
总结
event loop 作为面试的高频题,静下心来认真的分析一下,其实不难理解。
欢迎关注
欢迎关注公众号“码上开发”,每天分享最新技术资讯