一、js的事件循环
1、同步和异步
任务可以分为同步任务和异步任务
同步任务可以理解成立即执行任务,同步任务一般会直接进入主线程中进行执行,异步任务比如ajax网络请求,setTimeout定时函数都属于异步任务,异步任务会通过事件队列的机制来进行协调。
同步任务和异步任务进入不同的执行环境,同步进入主线程,即主执行栈,异步进入事件队列,主线程内的任务执行完成为空,就去查看事件队列里的任务,推入主线程,这个过程不断的循环执行就是事件循环。
借用网上博主的一篇流程图说明这个过程:
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线程继续接管,开始下一个宏任务
二、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)-->轮询阶段(按照该顺序反复运行)...
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 队列的任务。
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
Node 端运行结果:timer1=>timer2=>promise1=>promise2
全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;
首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,同样的步骤执行 timer2,打印 timer2;
至此,timer 阶段执行结束,event loop 进入下一个阶段之前,执行 microtask 队列的所有任务,依次打印 promise1、promise2
四、总结
浏览器和 Node 环境下,microtask 任务队列的执行时机不同
Node 端,microtask 在事件循环的各个阶段之间执行
浏览器端,microtask 在事件循环的 macrotask 执行完之后执行