浏览器和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