「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
1 前言
我们知道,JavaScript 引擎执行 JS 代码是单线程执行
单线程意味着有可能出现阻塞,那浏览器怎么解决阻塞的问题,答案是 Event Loop
Event Loop即事件循环,是浏览器或 Node 解决 JS 单线程运行时不会阻塞的一种机制
浏览器、Node 中对 JS 程序来说存在一个“主线程”(JS 引擎)以及一个“任务队列”(由其他线程维护,浏览器和Node略有不同)
2 同步任务和异步任务
JS 任务按执行时机分为同步任务和异步任务,同步任务指会立即执行的任务,异步任务指不会立即执行的任务(计时器、DOM事件监听或网络请求等任务)
同步任务会在执行栈中按顺序由主线程依次执行
异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(执行栈被清空),被读取到执行栈内等待主线程的执行
3 浏览器、Node执行JS的机制
-
同步任务在主线程执行,形成一个执行栈
-
主线程遇到异步任务时,会将他们交给对应的线程去处理,主线程继续执行后续任务
异步任务有了结果后,对应的线程会把回调函数交给任务队列去维护
-
执行栈中的所有同步任务执行完毕后,主线程会读取任务队列,若有任务会被读取到执行栈中进行执行
-
主线程不断重复第3步,这个运行机制就称为Event Loop(事件循环)
4 浏览器、Node.js的职责分配
4.1 浏览器的渲染进程(内核)
浏览器是多进程的,有一个主控进程,负责协调、主控
另外的进程有:第三方插件进程、GPU进程、浏览器渲染进程(内核)等
其中,浏览器渲染进程即内核进程,负责页面渲染、脚本执行、事件处理等
内核进程是多线程的,主要线程有:
-
GUI线程:用于监听和响应用户在图形用户界面上的操作
需要注意的是,该线程和JS引擎线程是互斥的,一个在执行时另一个会被挂起
-
JS引擎线程:即前述“主线程”,负责解释、执行JS代码
-
事件触发线程:维护任务队列
-
定时器线程
-
网络请求线程
浏览器的异步执行机制:JS引擎遇到异步,会交给对应的线程维护异步任务,异步任务有了结果之后,事件触发线程将异步对应的回调函数加入任务队列,任务队列中的回调函数等待被JS引擎执行
4.2 Node.js 的Event Loop
Node也是单线程的Event Loop,但是运行机制不同于浏览器环境
Node.js运行机制:
- V8引擎负责解析JS脚本
- 解析后的代码,调用Node API
- libuv库负责Node API的执行,它会将不同的任务分配给相应的线程,待其完成后分配各个请求的注册回调函数到任务队列,通知上层的JS引擎,空闲时捞起队列中的回调函数
4.3 浏览器和Node职责分配
浏览器:
- JS引擎线程:负责解释、执行JS代码,空闲时捞起任务队列中的任务执行
- 事件触发线程:负责维护任务队列,其他线程执行完异步任务后通知事件触发线程将回调函数放入任务队列,等待JS引擎执行
Node:
- V8引擎:负责将JS代码编译成本地机器码后执行机器码,空闲时捞起任务队列中的任务执行(提供了JS实时运行环境)
- libuv:负责将不同的任务分配给相应的线程,待其完成后将回调函数放入任务队列,等待V8引擎执行(处理异步事件)
5 宏任务和微任务
5.1 宏任务
宏任务,macrotask,也叫tasks,部分异步任务的回调会依次进入宏队列,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate(Node独有)
- requestAnimationFrame(浏览器独有)
- I/O
- UI rendering(浏览器独有)
5.2 微任务
微任务,microtask,也叫jobs,另外的异步任务的回调会依次进入微队列,等待后续被调用,这些异步任务包括:
- process.nextTick(Node独有)
- Promise
- MutationObserver
5.3 宏任务和微任务的先后执行顺序
前面所述的“任务队列”其实包括“宏任务队列”和“微任务队列”,这两者的优先级是“微任务队列”高于“宏任务队列”。执行步骤如下:
- 执行栈清空,JS引擎查看“微任务队列”是否有任务
- 若有,执行完“微任务队列”中所有任务,在这个过程中还可能不断向“微任务队列”中添加微任务,需要依次执行直至队列为空
- 执行“宏任务队列”中的第一个宏任务,在此过程中可能产生微任务,将微任务添加到“微任务队列”
- 执行完该宏任务后,重复上述步骤
简单来说,当执行栈清空时,JS引擎去查看任务队列时会优先查看“微任务队列”中是否有任务,当“微任务队列”执行完之后,才会去执行“宏任务队列”中的第一个宏任务,接着再去查看“微任务队列”执行微任务。即"微任务队列"——>"一个宏任务"——>"微任务队列"——>"一个宏任务"...,在node中略有不同,node中会执行完一种宏任务以后再去执行微任务队列
6 Node.js的Event Loop
前面已经提到了Node的Event Loop的运行机制是基于libuv实现的
6.1 Node的Event Loop的6个阶段(6种宏任务)
-
定时器(timers):本阶段执行已经被setTimeout和setInterval的调度回调函数(也就是那些到时间应该执行的定时器回调函数)
-
待定回调(pending callbacks):执行上一循环延迟的I/O回调
-
idle,prepare:仅系统内部使用
-
轮询(poll):检索新的I/O事件,执行与I/O相关的回调(除了关闭的回调函数以及那些由计时器和setImmediate调度的情况之外),其余情况node将在适当的时候在此阻塞
说明:
- 适当的时候:已经没有其他可执行的任务的时候,此时还有异步的I/O没有返回;此时程序会在此阻塞,等待I/O的回调
- 当队列用尽或达到回调限制,事件循环将移动到下一阶段执行,也就是待定回调(pending callbacks)阶段
-
检测(check):setImmediate回调函数在此执行
-
关闭回调函数(close callbacks):一些关闭的回调函数,例如socket.on或http.server.on
6.2 Node的任务队列和执行顺序
- 上述6个阶段,每个阶段都有一个任务队列,只有当这个队列执行完或者达到该阶段的任务队列的上限时,才会进入下一个阶段
- 执行完每个阶段的任务队列后,会去清空微任务队列,再进入下一个阶段
6.3 几个问题
6.3.1 setTimeout和setImmediate执行顺序
分两种情况
- 当 setTimeout() 和 setImmediate() 都写在 main 里面的时候 不一定谁先执行谁后执行;这个时候受线程性能的影响。setTimeout 0 在node中会被设置为1ms后执行。时间循环的准备需要时间,如果事件循环的准备时间大于1ms那 setTimeout 0先执行。反之 setImmediate 先执行。
- 当 setTimeout() 和 setImmediate() 都写在一个 I/O 回调 或者说一个 poll 类型宏任务的回调里面的时候 一定是先执行 setImmediate() 后执行 setTimeout()
6.3.2 process.nextTick和setImmediate
setImmediate可理解为是个宏任务,在check阶段触发执行。
process.nextTick可以理解为是一个微任务,我们知道微任务是在宏任务执行的间隙去执行,所以当一个(种)任务队列执行完毕后,会执行微任务;而process.nextTick会最优先被执行。