node事件循环
主要是介绍了 node 事件循环,以及 node 是否是多线程
# 事件循环(拉勾的)
- 参考这里 (opens new window)
- 虽然事件循环的起始点是 timer,但是 timer 里的代码会被加入到 poll 里属于宏任务。所以还是严格按照 poll 里的执行顺序。
- 自注:主要是 poll 阶段的执行过程,归根到底同一层的事件循环还是执行过程如下:
- 先执行主线程同步代码
- 然后执行异步线程
- 先执行微任务(process.nextTick 的优先级 高于 Promise)
- 然后执行宏任务()
- 谁先达到执行条件谁就先执行?异步任务的宏任务里 fs.readFile 的优先级高于 setTimeout。但是如果 fs.readFile 执行的时间大于 setTimeout 等待的时间,那么这里就先执行 setTimeout 再执行 fs.readFile。
- 第一层事件循环的优先级高于第二层事件循环,以此类推。也就是如果第一层的异步任务里又注册有异步任务(我们统称第二层事件循环),那么必须等到第一层异步任务都执行完再执第二层事件循环。
- Node.js 是单线程的还是多线程的:
- 主线程是单线程执行的,但是 Node.js 存在多线程执行,多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化等。这里也可以解释我们前面提到的第 4 个问题,主要还是主线程来循环遍历当前事件。
# node 中的 nextTick(珠峰)
笔记在:https://lqyld.github.io/document/NodeJs/introduce/#%E4%BA%94-node%E4%B8%AD%E7%9A%84event-loop 这是别人的笔记 笔记很好基本随着珠峰的课程。来源于 github:https://github.com/LQYld/lqyld.github.io
代码在:https://gitee.com/wangluoshihuang/zhufeng202103node/blob/master/3.node-core/1.js
nextTick:在 node 中是 node 自己实现的,不属于 node 中的 EventLoop,优先级比 promise 更高。nextTick 里面的代码被放在执行栈的底部——同步代码执行完后立即执行;执行优先级比 eventLoop 更高。
# node 中的 eventLoop(珠峰)
- node 中的 eventLoop 里我们呢只需要关心:timers、poll、check、close callbacks 这几个阶段。其他的阶段我们操作不了。
- 官网链接 (opens new window)
- timers:主要指定时器
- 有一个现象就是:我们在全局代码里同时有 setTimeout 以及 setImmediate,按照事件循环本应该是先执行 timers 再执行 setImmediate;但是我们多次刷新之后,发现有那么一次是 setImmediate 先执行。为什么呢?很可能是代码执行的时候定时器还没有到时间,所以 timer 阶段就跳过去了没有执行,这个 timer 只有在下一次事件循环执行。
- 但是有一种情况,setImmediate 和定时器【定时器定时为 0】的执行顺序是固定的(setImmediate 优先级更高):i/o 里的 setImmediate 和定时器的执行顺序是固定的 (opens new window)。
- poll:指 I/O 的回调函数
- poll 会是一个等待的过程
- 当 eventLoop 走到这一步时,如果有异步操作时就会呈现一个等待的过程,直到到达异步后续的执行条件时 eventLoop 再往后走。
- poll 阶段细节:
- 1.检测 poll 队列中是否为空,如果不为空则执行队列中的任务,知道超时或全部执行完毕
- 2.执行完毕后检测 setImmediate 队列是否为空,如果不为空则执行 check 阶段;如果为空则看有没有定时器,有的话则等待时间到达,时间达到后回到 timer 阶段。
- 3.等待时间到达时可能会出现新的 callback,此时也在当前阶段被清空。
- 例如在等待 setTimeout 的同时,又执行了文件读取,那么等待时间达到后,定时器以及这个 i/o 回调函数都会被执行。
- 上面的执行过程代码参考:https://gitee.com/wangluoshihuang/zhufeng202103node/blob/master/3.node-core/1.js#L89
- 也就是说在 poll 阶段时代码的执行过程有 2 中:
- 第一种:poll 阶段之后没有 setImmediate,那么 eventLoop 就直接返回到 timer 阶段再执行(下一次事件循环开始)。
- 第二种:poll 阶段之后有 setImmediate,那么 eventLoop 就继续往下走。
- 假如异步 i/o 之后紧挨着的是定时器,那么定时器和异步 i/o 都会在 poll 阶段呈现一个等的状态,谁先到达执行的条件谁就先执行。
- poll 会是一个等待的过程
- check:指 setImmediate()函数里的代码的。
- close callbacks:关闭的回调函数。
# 事件循环总结(珠峰)
- 浏览器的特点是: 先执行执行栈中代码,【宏任务执行完后->清空微任务队列 ->取出一个异步宏任务来执行(代码执行是在执行栈中执行)】以此 不停的循环。
- node 的特点:先执行当前执行栈代码,【宏任务完毕后, 执行 nextTick,清空微任务队列,进入到事件环中 拿出一个异步宏任务来执行 】以此不停的循环。
- 但是在早期不是这样的(早期有区别 11+)——早期是每个队列(例如 node 有 6 个队列)清空完接着马上清空微任务,再清空下一个队列,以此类推。
- 现在是:在 eventLoop 里每执行某个队列里的某个任务之后,那么紧接着就会清空微任务队列。例如在 timer 队列里有 5 个定时器,那么每个定时器执行之后紧接着清空微任务队列,然后再执行下一个定时器再清空一次微任务队列,以此类推直到 timer 队列里 5 个定时器执行完走下一个阶段的 eventLoop。
# 这个版本和上面的结合会理解的更清楚
- 相应笔记:这里 (opens new window)
- 异步操作都会跑到异步 i/o 里等待被相应阶段取出来用。
- poll 阶段是重点:重点理解 poll 阶段。
- poll 阶段可能会处于一个等待【阻塞】状态:
- 呈现等待状态的:poll 队列为空【异步 i/o 回调函数队列为空】,尝试走 check 阶段发现 i/o 里没有 immediate 回调函数,也不会再有异步 i/o 回调函数达到执行条件,也没有定时器回调函数到达执行条件。否者只要不满足任意一个条件,那么等待的状态就会被打破,继续走对应情况【上面笔记讲的很清楚,参考‘如果 event loop 进入了 poll 阶段’这就话】的下一步。
- poll 阶段可能会处于一个等待【阻塞】状态:
- 主要原则就是:事件循环一定是按照步官网流程走的。异步操作(setTimeout、setInterval、异步 i/o、nextTick)的回调函数都会被放入 i/o 里面。
- 事件循环的每个阶段都会去询问 i/o 里有没有这个阶段对应可执行的函数【有的话就执行】,跳入下一个阶段之前先寻找是否存在 nextTick 函数的回调函数【有的话就执行】,然后执行下一阶段。
- 在 eventLoop 里每执行某个队列里的某个任务之后,那么紧接着就会清空已经存在的微任务队列。例如在 timer 队列里有 5 个定时器,那么每个定时器执行之后紧接着清空微任务队列,然后再执行下一个定时器再清空一次微任务队列,以此类推直到 timer 队列里 5 个定时器执行完走,才会走 event-loop 的下一个阶段。
- 事件循环的每个阶段都会去询问 i/o 里有没有这个阶段对应可执行的函数【有的话就执行】,跳入下一个阶段之前先寻找是否存在 nextTick 函数的回调函数【有的话就执行】,然后执行下一阶段。
# 事件循环可视化
- https://www.jsv9000.app/
# 自己归纳的事件循环执行过程
事件循环的执行过程:主线程同步代码执行=》主线程为空=》异步微任务=》异步宏任务 。以这个流程作为循环运行。
在 JavaScript 引擎有一个监控进程,不断检查主线程执行栈是否为空。一旦为空,它就会去事件队列检查是否有任何函数正在等待调用。
setTimeout(() = > { console.log('A') }, 100) new Promise((resolve) = > { console.log('B') resolve() }) .then(() = > console.log('C')) setTimeout(() = > { console.log('D') }, 0) async function main () { await Promise.resolve() console.log('E') } main() console.log('F') // 结果为: B F C E D A1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这里真正讲清楚了 js 和 node 事件循环:
常见的异步任务:
常见的宏任务有:script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
- 宏任务 setTimeout 的误区:
- setTimeout 的回调不一定在指定时间后能执行。而是在指定时间后,将回调函数放入事件循环的队列中。
- 如果时间到了,JS 引擎还在执行同步任务,这个回调函数需要等待;如果当前事件循环的队列里还有其他回调,需要等其他回调执行完。
- setTimeout 0ms 也不是立刻执行,它有一个默认最小时间,为 4ms。因为取出第一个宏任务之前在执行全局 Script,如果这个时间大于 4ms,这时 setTimeout 的回调函数已经放入队列,就先执行 setTimeout;如果准备时间小于 4ms,就会先执行 setImmediate。
- 宏任务 setTimeout 的误区:
常见的微任务有:process.nextTick(node 独有)/Promise.then()/Object.observe/MutationObserver
什么是一个事件循环:
- 事件循环由宏任务和在执行宏任务期间产生的所有微任务组成。完成当下的宏任务后,会立刻执行所有在此期间入队的微任务。
浏览器的事件循环:
浏览器的事件循环由一个宏任务队列+多个微任务队列组成。
首先,执行第一个宏任务:全局 Script 脚本。产生的的宏任务和微任务进入各自的队列中。执行完 Script 后,把当前的微任务队列清空。完成一次事件循环。
接着再取出一个宏任务,同样把在此期间产生的回调入队。再把当前的微任务队列清空。以此往复。
宏任务队列只有一个,而每一个宏任务都有一个自己的微任务队列,每轮循环都是由一个宏任务+多个微任务组成。
Promise.resolve().then(() => { console.log("第一个回调函数:微任务1"); setTimeout(() => { console.log("第三个回调函数:宏任务2"); }, 0); }); setTimeout(() => { console.log("第二个回调函数:宏任务1"); Promise.resolve().then(() => { console.log("第四个回调函数:微任务2"); }); }, 0); // 第一个回调函数:微任务1 // 第二个回调函数:宏任务1 // 第四个回调函数:微任务2 // 第三个回调函数:宏任务21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Node 的事件循环:
- 参考 (opens new window) 中的 'Node 的事件循环'
- 里面讲有关 setTimeout 设置为0和setImmediate的执行顺序经过node23 测试后,与文章中的结果不一样。可能可以用上面的 '宏任务 setTimeout 的误区'解释。
- 文章中执行规律,自己总结就是:总共有6个阶段,每个阶段包含宏任务和对应阶段的微任务。每个阶段的执行顺序是:每个阶段先执行当前阶段的同步代码,然后执行process.nextTick,然后清空微任务,最后进入下一个阶段。
- 参考 (opens new window) 中的 'Node 的事件循环'