浏览器和node的事件循环
参考博客:浏览器和Node事件循环的区别
总结:
-
浏览器和Node事件循环的区别

Event Loop即事件循环,是指浏览器 或 Node的一种 解决javaScript单线程运行时不会阻塞 的一种机制,也就是我们经常使用异步的原理。
一、浏览器事件循环
1、JS的主线程是【单线程】
但是 ajax和 setTimeout就会在浏览器中多开一条线程;
JS有一个主线程 ,所有的任务都会被放到调用栈等待主线程执行;
2、同步和异步

- 同步和异步任务 分别进入不同的执行”场所” ,同步的进入主线程,异步的进入
Event Table并注册函数 -
同步任务会在调用栈中按照顺序等待主线程依次执行
-
异步任务会在 异步任务有了结果后,Event Table会将回调函数移入
Event Queue -
主线程内的任务执行完毕(调用栈被清空),会去
Event Queue读取对应的函数,进入主线程执行。 - 上述过程会不断重复,也就是常说的 Event Loop(事件循环)。
两个定时函数:
-
setTimeout是经过指定时间后,把要执行的任务加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于设置的时间长度。
-
setInterval会每隔 指定的时间将注册的函数置入Event Queue ,如果前面的任务耗时太久,那么同样需要等待。
唯一需要注意的一点是,对于
setInterval(fn,ms)来说,我们已经知道不是每过ms秒会执行一次fn,而是 每过ms秒,会有fn进入Event Queue。—> 一旦 setInterval的回调函数fn执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔了。
3、宏任务和微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务):包括整体代码
script,setTimeout,setInterval,setImmediate(ie下生效)MessageChannel(消息通道),I/O,UI Rendering - micro-task(微任务):
Promise.then(async/await),Process.nextTick(node的文法,比Promise.then执行的快),MutationObserver(监听dom节点更新完毕)async/await:底层转换成了Promise和then回调函数 —> 每次我们使用await, 解释器都创建一个 promise 对象,然后把剩下的async函数中的操作放到 then 回调函数中- Promise 的
then和catch才是 microTask ,本身的内部代码不是
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
拼多多编程题:

考点:
new Promise - 一注册就执行;
await 后面跟的函数是同步执行的,但是 async 中 await 之后的操作被放到了 then 方法中,即微任务中;
浏览器打印结果:

编程题2:

考点: Promise.resolve() 是一个同步方法,then 注册的方法才是微任务;
异步任务会先在 Event Table 中注册函数,但是只有结果返回后才会将函数推入事件队列【时机】;
执行分析:微任务队列内容循环
// 第一个微任务
console.log(1);
Promise.resolve().then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(4);
})
// ①执行输出1
// Promise.resolve()后,2被推入微任务执行队列
// 然后,第一个微任务的同步任务执行完,5 被推入微任务执行队列
// 按序执行两个已经在微任务队列中的值
// ②执行输出2(触发3被推入微任务队列)
// ③执行输出5(6被推入微任务执行队列)
// 按序执行两个已经在微任务队列中的值
// ④执行输出3(4被推入微任务执行队列)
// ⑤执行输出6
// 按序执行已经在微任务队列中的值
// ⑥执行输出4
浏览器打印结果:125364
二、node事件循环
node 环境和 浏览器环境的区别
- node - 事件循环实现依赖于 libuv 引擎
- 浏览器 - V8引擎将 js 解析后调用对应的 node API -> 最终都是由 libuv 引擎驱动
循环之前:
在进入第一次循环之前,会先进行如下操作:
- 同步任务;
- 发出异步请求;
- 规划定时器生效的时间;
- 执行
process.nextTick()。
开始循环:
循环中进行的操作:
- 清空当前循环内的 Timers Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 I/O Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 Check Queue,清空 NextTick Queue,清空 Microtask Queue;
- 清空当前循环内的 Close Queue,清空 NextTick Queue,清空 Microtask Queue;
- 进入下轮循环。
可以看出,nextTick 优先级比 Promise 等 microTask 高,setTimeout和setInterval优先级比setImmediate高。
node的事件环和我们浏览器的不太一样,它 给每一个任务都配了一个队列,如下图

外部输入数据 —> 轮询阶段(poll) —> 检查阶段(check) —> 关闭事件回调阶段(close callback) —> 定时器检测阶段(timer) —> I/O事件回调阶段(I/O callbacks) —> 闲置阶段(idle, prepare) —> 轮询阶段…
-
timers: 执行定时器
setTimeout和seInterval的回调 -
I/O callbacks: 这个阶段执行几乎所有的回调 - 是否有已完成的 I/O 操作的回调函数,来自上一轮的 poll 残留。但是 不包括 close事件,定时器和
setImmediate()的回调。 -
idle, prepare: 内部的一些事件。
-
poll: 轮循,i/o,回调,fs.readFile()。
—> 先查看
poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。—> 当
queue为空时, —> 会检查是否有
setImmediate()的callback,如果有就进入check阶段执行这些callback。 但同时也会检查是否有到期的
timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的callback。 这 两者的顺序是不固定 的,收到代码运行的环境的影响。
—> 如果两者的queue都是空的,那么
loop会在poll阶段 停留,直到有一个 i/o 事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback -
check:执行
setImmediate的回调。 -
close callbacks: 一些关闭的回调函数,例如
socket.on('close', ...)/socket.destory()。
需要关心的就是 timers、poll、check 这三个阶段,执行顺序默认,会从上到下依次执行,
1、如果代码执行到 poll 后,发现check阶段没有,那就在poll在等待,等待 timer 时间到达后,再清空代码
2、队列(Timers Queue、I/O Queue、Check Queue 和 Close Queue)发生切换时,就会执行微任务
3、poll 的下一个阶段就是check,如果check队列中有东西的s,会先执行check

三个常用于推迟任务执行的方法 - process.nextTick、setTimeout、setImmediate
这三者间存在着一些非常不同的区别
| process.nextTick() | setTimeout | setImmediate |
|---|---|---|
nextTick queue - 在每一个阶段执行完毕准备进入下一个阶段时优先执行(类似于微任务 -> 循环执行,直到清空) |
定义一个回调,并且希望这个回调在我们所指定的时间间隔后 第一时间 去执行 -> 受到操作系统、当前执行任务的影响,并不会在预期的时间精准执行 | 立刻执行 - 在 poll 阶段之后才会执行回调 |
代码案例1:
// node 版本 12.13.1
setTimeout(()={
console.log("time1");// --> 第3
process.nextTick(()=>{
console.log("nextTick2");// --> 第4【类似于微任务】
});
});
console.log("start") // --> 第1
process.nextTick(()=>{
console.log("nextTick1"); // --> 第2
setTimeout(()={
console.log("time2"); // --> 第5
});
});

代码案例2:在 node 中执行以下代码结果是什么 –> 答案是不一定【配合看案例3】
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});

代码案例3:【配合看案例2】
在一种情况下可以准确判断两个方法回调的执行顺序:在 同一个I/O事件的回调 中
因为在I/O事件的回调中,setImmediate 方法的回调永远在 timer 的回调前执行。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 答案永远是: immediate timeout