js|js和node的事件循环区别🆚🆚

501 阅读4分钟

一、js的事件循环

1、同步和异步

任务可以分为同步任务和异步任务

同步任务可以理解成立即执行任务,同步任务一般会直接进入主线程中进行执行,异步任务比如ajax网络请求,setTimeout定时函数都属于异步任务,异步任务会通过事件队列的机制来进行协调。

同步任务和异步任务进入不同的执行环境,同步进入主线程,即主执行栈,异步进入事件队列,主线程内的任务执行完成为空,就去查看事件队列里的任务,推入主线程,这个过程不断的循环执行就是事件循环。

借用网上博主的一篇流程图说明这个过程:

image (14).png

2、宏任务和微任务

js的异步任务中才分宏任务和微任务

js分为2种任务类型宏任务(macrotask)和微任务(microtask)在ES中,宏任务称为task,微任务称为jobs

宏任务:每次执行栈执行的代码就是一个宏任务(包括从事件队列中获取一个事件回调并放到执行栈执行)

每一个宏任务会从头到尾把这个任务执行完成,不会执行其他

浏览器为了能够使得js内部的宏任务和DOM任务有序的执行,会在一个宏任务结束之后,下一个宏任务开始之前,对页面进行重新渲染(task-->渲染-->task-->...)

微任务:可以理解成在当前宏任务结束后立即执行的任务,也就是说在当前宏任务结束后,下一个宏任务之前,在渲染之前

所以微任务的响应速度相比setTImeout要快,因为不需要等待渲染,也就是说在某一个宏任务执行完成后,就会立即执行在它执行期间产生的所有微任务,(在渲染前)

宏任务主要包括:script整体代码、setTimeout、setInterval、I/O、UI交互事件、setImmediate

微任务主要包括:Promise、MutaionObserver、process.nextTick(Node.js 环境)

3、小结

执行一个宏任务(如果没有就从事件队列中读取)

执行过程中如果遇到微任务,就添加到微任务的事件队列中

宏任务执行完成,立即执行当前微任务队列中的所有微任务(依次执行)

当宏任务执行完成,开始检查渲染,然后GUI线程接管渲染,渲染完成后,js线程继续接管,开始下一个宏任务

image (15).png

二、node的事件循环

1、node运行机制

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuv,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现

Node.js 的运行机制如下

V8 引擎解析 JavaScript 脚本。

解析后的代码,调用 Node API。

libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。

V8 引擎再将结果返回给用户。

2、六个阶段

从图中,大致看出 node 中的事件循环的顺序: 外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...

image (16).png

timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调

I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调

idle, prepare 阶段:仅 node 内部使用

poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里

check 阶段:执行 setImmediate() 的回调

close callbacks 阶段:执行 socket 的 close 事件回调

三、两者区别

1、图示

浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。

image (17).png

2、例子

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

浏览器端运行结果:timer1=>promise1=>timer2=>promise2

20211015215432.gif

Node 端运行结果:timer1=>timer2=>promise1=>promise2

全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;

首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;

至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2

20211015215458.gif

四、总结

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

Node 端,microtask 在事件循环的各个阶段之间执行

浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

参考👀

浏览器与Node的事件循环(Event Loop)有何区别?