事件循环是浏览器和Node用来解决JS单线程运行带来的问题的一种运行机制。浏览器和NodeJS环境下的事件循环是不同的,浏览器是完全遵循HTML5规范去实现的,NodeJS的事件循环是基于libuv实现的,在HTML5规范的基础上做了些取舍,为了保证上层(即javascript)语法一致,API方面和浏览器差不多,但是宏/微任务的执行逻辑和浏览器完全不一样。比如NodeJS新增了nextTick、setImmediate,而没有MutationObserver。
浏览器和NodeJS下宏任务和微任务分别有:
1、浏览器端只有IE才有setImmediate,NodeJS端又启用了它。
2、MutationObserver用来观察Dom节点变化,包括子节点的删除、属性修改、文本内容修改等等。
3、requestAnimationFrame(foo)涵义是延迟执行foo,而延迟多长时间由浏览器根据操作系统当前的显示器刷新率来决定,从而保证它的执行和显示器刷新频率保持一致,避免丢帧现象。解决了用setTimeout制作动画时自行设定延时不准确而丢帧的问题,所以requestAnimationFrame不是setTimeout的替代,而是特定场景下的补充。需要注意的是这个方法虽然能够保证回调函数在每一帧内只渲染一次,但是如果这一帧有太多任务执行,还是会造成卡顿的;因此它只能保证重新渲染的时间间隔最短是屏幕的刷新时间。
4、我不认为requestAnimationFrame和UI rendering属于宏任务,所以划掉了。
怎么看UI render呢?
网上不少地方说UI render(也就是绘制界面)也列为宏任务,但我从HTML标准里看到,其实UI render和宏任务是平行的,完整的事件循环是:执行第一个宏任务 -> 执行所有微任务 -> UI render -> 执行下一个宏任务...。重点看UI render,HTML标准里提供了一段伪代码,用来说明UI render阶段干了什么:
1、我们看一看IntersectionObserver,它是用来监听元素是否出现(出现的比例)在屏幕视口的。
2、我们再看一个也会产生异步任务的API,requestIdleCallback。简单来说,它是让回调函数在进程空闲时再执行,如果进程忙的话就延后到下一次。可以传一个timeout时间requestIdleCallback(foo, {timeout:6000}),表示6000ms以后就算进程忙也要强制执行。那么怎么才算进程不忙呢?HTML标准做了说明:当宏任务和微任务全部执行完了才算。另外也可以看出,requestIdleCallback的回调是在UI render的最后一步(第12步)才执行的,而且还有可能不执行,优先级非常之低。
NodeJS的事件循环
NodeJS 采用 V8 作为JS解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。在浏览器中,可以认为只有一个宏队列,所有的macrotask都会被加到这一个宏队列中,但是在NodeJS中,不同的macrotask会被放置在不同的宏队列中,有
1、Timers Queue
;(放置setTimeout、setInterval的回调)
2、IO Callbacks Queue
;
3、Check Queue
;(放置setImmediate的回调)
4、Close Callbacks Queue
;
在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中,但是在NodeJS中,不同的microtask会被放置在不同的微队列中,所以执行微任务的时候,NodeJS会优先执行NextTick Queue里的所有任务,再执行Other Micro Queue的所有任务。
1、NextTick Queue
;(放置process.nextTick的回调)
2、Other Micro Queue
;(放置Promise等其他microtask)
所以,NodeJS环境的事件循环是: