浏览器中的Event Loop事件循环
当代码需要进行一项异步任务时,主线程将这些代码放到幕后线程中去执行,主线程继续执行栈中剩余的代码。当幕后线程(background thread)里的代码执行完成后(比如setTimeout时间到了,ajax请求得到响应),该线程就会将它的回调函数放到任务队列(又称作事件队列、消息队列)中等待执行。而当主线程执行完栈中的所有代码后(即主线程空闲后),它就会检查任务队列是否有任务要执行,如果有任务要执行的话,那么就将该任务放到执行栈中执行。如果当前任务队列为空的话,它就会一直循环等待任务到来。因此,这叫做事件循环。
遇到异步任务—>交给幕后线程—>幕后线程执行完毕时将其回调函数放入任务队列—>主线程空闲时查找队列是否有任务—>有则拉到栈中执行、无则循环等待
-
任务队列
-
Macrotask(宏任务) :
- script(整体代码)
- setTimeout
- setInterval
- I/O
- UI交互事件
- postMessage
- MessageChannel
-
Microtask(微任务) :
- Promise.then(重点)
- process.nextTick(nodejs)
- Object.observe
-
-
事件循环执行流程
一次事件循环只执行位于Macrotask队首的任务,执行完成后立即执行Microtask队列中的所有任务(一开始在js主线程中跑的任务就是Macrotask任务,因此执行完主线程的代码后,会从Microtask队列中取任务来执行)
-
定时器问题:setTimeout不保证可靠定时
定时器中设置的时间仅保证任务会在delay毫秒后进入Macrotask队列,并不意味着它能立刻运行,因为可能当前主线程正在进行一个耗时的操作,也可能目前Microtask队列中有很多个任务。
-
Js是阻塞还是非阻塞的?
核心是同步阻塞,而对于js异步事件,因为有事件循环机制,所以异步事件就是由事件驱动异步非阻塞
-
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成(死等该事件)
-
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成(轮询)
-
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音(死等关联事件,最傻)
-
异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。(等通知,最机智)
-
-
requestAnimationFrame既不属于Microtask也不属于Macrotask
同步任务→promise等微任务→制作render树→requestAnimationFrame→制作render树→第一帧重绘完成→setTimeout等宏任务
Node中的Event Loop事件循环
-
Node简介
Node.js采用V8作为js的解析引擎,而I/O处理方面使用libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制由它实现
-
Node.js运行机制
- V8引擎解析JavaScript脚本
- 解析后的代码调用Node API
- libuv库负责Node API的执行,它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给V8引擎
- V8引擎将结果返回用户
-
事件循环六个阶段
libuv中的事件循环分六个阶段,它们会按照顺序反复运行,每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量达到系统设定的阈值,就会进入下一阶段。
-
timers阶段
执行setTimeout()、setInterval()的回调,由poll阶段控制(与浏览器不同,timers阶段有几个setTimeout、setInterval都会依次执行)
-
I/O callbacks阶段:处理上一轮循环中少数未执行的I/O回调
-
idle,prepare阶段:仅node内部使用
-
poll阶段
获取新的I/O事件,适当的条件下node将阻塞在这里。
该阶段系统会做两件事:回到timer阶段执行回调;执行I/O回调。在进入该阶段时:
-
如果没有设定timer,则:
1.若poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制;
2.若poll队列为空,则
- 若有setImmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调;
- 若没有setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,同时会有个超时时间防止死等。
-
如果设定了timer且poll队列为空,则会判断是否有timer超时,如果有的话会回到timer阶段执行回调
-
-
check阶段:执行setImmediate()的回调
-
close callbacks阶段:执行socket的close事件回调
-
-
MicroTask与MacroTask
MacroTask:setTimeout、setInterval、setImmediate、script(整体代码)、I/O操作
MicroTask:process.nextTick、Promise().then
-
注意点
-
setTimeout和setImmediate
-
两者调用时机不同
- setImmediate设计在poll阶段完成时执行,即check阶段;
- setTimeout设计在poll阶段为空闲时,且设定时间达到后在timer阶段执行。
-
二者在异步I/O callbacks内部调用时,总是先执行setImmediate再执行setTimeout;
-
其他情况先后顺序不一定(setTimeout(func, 0)===setTimeout(func, 1),如果在准备时候花费时间大于1ms,则在timers阶段就会直接执行setTimeout回调,小于1ms先执行setImmediate回调)
-
-
process.nextTick
独立于Event Loop之外,有一个自己的队列,当每个阶段完成后如果存在nextTick队列,就会转而清空nextTick队列中的所有回调函数,且优先于其他microtask执行
Node与浏览器的Event Loop差异
-
Microtask任务队列的执行时机不同
- Node端:microtask在事件循环的各个阶段之间执行(node10 及以下timers阶段有几个setTimeout/setInterval都会先依次执行再执行MicroTask) (node 11则和浏览器一致)
- 浏览器端:microtask在事件循环的每个macrotask执行完之后执行
-
同:
执行完主线程任务,都首先执行microtask
例子:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>promise1=>timer2=>promise2 游览器和node11
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2 node10及以下
参考: