游览器事件循环
宏任务/微任务
除了广义的同步任务和异步任务,我们对任务有更精细的定义:
- macro-task(宏任务):当前调用栈中执行的任务称为宏任务。包括:setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。宏任务队列可以有多个
- .micro-task(微任务): 当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务为微任务。包括:Promise的then回调、Object.observe(废弃)、MutationObserver
- 不同类型的任务会进入对应的Event Queue,宏任务中的事件放在callback queue中,由事件触发线程维护;微任务的事件放在微任务队列中,由js引擎线程维护。微任务队列只有一个
什么是event loop
- 主线程运行的时候会生成堆(heap)和栈(stack);
- js 从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;函数开始和结束时,也会进行压栈、出栈操作。
- 当程序调用外部的 API 时(比如 ajax、setTimeout 等),会将此类异步任务挂起,继续执行执行栈中的任务。等异步任务返回结果后,再按照顺序排列到事件队列中;
- 主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
- 主线程每次将执行栈(main script)清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个循环往复的过程被称为“Event Loop 事件循环”
async函数内await之后的代码相当于在then回调,是微任务
node事件循环
游览器事件循环根据html5规范,而node事件循环是使用libuv(专注于异步IO)实现。node中的文件操作其实是libuv对操作系统的系统调用。
node事件循环tick分成很多阶段(宏任务队列)逐步进行:
- timers: 定时器相关回调
- pending callback: 一些系统操作的回调,如(tcp错误类型)
- idel,prepare:仅系统内部使用
- poll: 检索新IO事件,执行IO相关回调。poll阶段是耗时最多,它会停留,让IO回调尽早响应
- check: 执行setImmediate回调
- close callback:如 socket.on(‘close’,…) 等关闭回调
在初始化事件循环后定时器可能未将回调放入timer(即使定时为0),此时定时器回调会在下一次tick再执行。而check检测阶段在当前tick会立即执行。
因此定时器和setImmediate回调执行顺序即使定时器定时为0、代码相同也可能顺序不一致。但是过程还是根据阶段一步步进行的。
练习:
1 | const EventEmitter = require('events') |
同一个promise有多次同级then调用,这些then回调都会按顺序放在同一个微任务执行栈中依次进行(同级),但不是等到下次微任务栈才执行下个then。
ee.emit('event')
触发e.on('event', cb)
cb回调属于普通任务,不会进行事件循环等待
- 本文作者: MR-QXJ
- 本文链接: https://mr-qxj.github.io/2021/06/29/语言/EventLoop/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!