最近换工作面试,面试官问了一个node的事件循环,我竟然发现我只留下一丁点记忆,明明自己还写过相关的博客,真的恍若隔世,自从我服用了帕罗西丁之后,记忆力确实已经大不如前了。为了加强记忆,我再重新写一遍吧。
node时间驱动理解
基于事件驱动:webServer收到请求,把它关闭后进行处理,然后去服务下一个web请求,当请求完成,结果集被放到处理队列,当到达队列列头,把结果集返回用户,因为webServer一直接受请求而不等待任何读写操作,所以被称为驱动io,也叫非阻塞io:
生成一个主循环来监听事件,检测到event时触发回调函数,这类似于观察者模式,eventEmitter触发指定路由,而所有注册到这个事件上的处理函数就相当于观察者。 nodejs中有很多内置事件,可以通过event模块实例化来绑定和监听事件(其实就是路由的一些操作)
const events = require('events');
const eventEmiter = new events.EventEmitter();
eventEmiter.on('con1', data=>{
console.log('c1 suc', data);
eventEmiter.emit('data1 revice')
})
eventEmiter.on('con2', data=>{
console.log('c2 suc', data);
eventEmiter.emit('data2 revice')
})
eventEmiter.emit('con1');
emit理解比较抽象,不像浏览器的事件驱动或者点击这些,而是从事件源,类型,处理程序三要素理解。
集成Emitter的实例会有一个_events属性,指向空对象,用于缓存事件类型和时间2处理程序,事件类型就是空对象的key,处理程序就是value(数组包括,可多个)。有_eventCount(注册事件个数),_eventCount(记录每个事件最多可以注册多少个处理程序)
重要方法:on,off,once(只执行一次,完成后退出处理程序队列)
node中有很多内置事件,newListers,removeListener等
事件循环理解
node的事件循环由libuv库实现,浏览器的事件循环由浏览器辅助实现。
基础阶段
node事件循环分六个阶段,每个阶段都可以认为是一个宏任务
- time阶段:处理timeout,interval回调,由poll阶段控制
- io callback阶段:处理系统级别回调,如io读写回调,tcp失败回调
- idle,prepare阶段:node内部使用,可忽略
- poll阶段:处理io操作回调,计算应该阻塞等待I/O的时间,如果poll队列空了,在有setImmediate的时候会进入check阶段,否则进入空闲阶段。
- 空闲阶段:暂停,等待新回调加入
- check阶段:执行setImmediate阶段
- close callback阶段:执行关闭回调,如socket.on('close',()=>{})等
在这流程基础上有几个奇怪的执行时机:
特殊阶段
setImmediate
他在check阶段执行,下有例子:
setTimeout(() => {
console.log('定时器');
});
setImmediate(() => {
console.log('setImmediate');
});
// 输出不确定,两种都有可能
fs.readFile('./1.py', () => {
setTimeout(() => {
console.log('定时器');
});
setImmediate(() => {
console.log('setImmediate');
});
// 输出setImmediate,定时器,因为check阶段会在io阶段后循环到位
})
nextTick
不在事件循环任何阶段执行,而是穿插在阶段的切换期执行,他有自己的执行队列,微任务里,有两个执行队列:nextTickQueue和microTaskQueue。
小测试
setImmediate(() => console.log('Immediate1'));
setImmediate(() => {
console.log('Immediate2');
Promise.resolve().then(() => console.log('promise1'));
});
setImmediate(() => console.log('Immediate3'));
setImmediate(() => console.log('Immediate5'));
process.nextTick(() => {
console.log('nextTick1');
});
process.nextTick(() => {
console.log('nextTick2');
Promise.resolve().then(() => console.log('promise2'));
});
process.nextTick(() => {
console.log('nextTick3');
});
process.nextTick(() => {
console.log('nextTick5');
});
上面输出了nextTick1,nextTick2,nextTick3,nextTick5,promise2,Immediate1,Immediate2,promise1,Immediate3,Immediate5
// 代码块1
setTimeout(function () {
//宏任务1
console.log('1');
});
new Promise(function (resolve) {
console.log('2'); //同步任务1
resolve();
}).then(function () {
//微任务1
console.log('3');
});
console.log('4'); //同步任务2
// 代码块2
setTimeout(function () {
//宏任务2
console.log('5'); //宏任务2中的同步任务
new Promise(function (resolve) {
console.log('6'); //宏任务2中的同步任务
new Promise(function (resolve) {
//宏任务2中的微任务
console.log('x1');
resolve();
}).then(function () {
console.log('X2');
});
setTimeout(function () {
//宏任务2中的宏任务
console.log('X3');
new Promise(function (resolve) {
//宏任务2中的宏任务中的同步任务
console.log('X4');
resolve();
}).then(function () {
//宏任务2中的宏任务中的微任务
console.log('X5');
});
});
resolve();
}).then(function () {
//宏任务2中的微任务
console.log('7');
});
});
// 代码块3
setTimeout(function () {
//宏任务3
console.log('8');
});
对于这段代码node环境和浏览器环境输出一致,输出答案:2,4,3,1,5,6,x1,x2,7,8,x3,x4,x5
浏览器事件循环
基本架构是调用栈(call stack)+任务队列(task queue)+webAPIs
其中任务队列分为:微任务队列如Promise.then/catch/finally、MutationObserver、queueMicrotask()和宏任务队列如setTimeout、setInterval、setImmediate(非标准)、requestAnimationFrame、I/O、UI渲染
具体的执行流程是:
- 执行同步代码---入调用栈
- 遇到异步API,交给WEBAPI处理,回调函数放入对应队列
- 同步代码执行完毕,调用栈清空
- 检查微任务队列,执行完所有微任务
- 在宏任务队列中执行第一个宏任务
- 重复步骤4-5(每次执行完宏任务后都清空微任务队列)
- 必要时进行UI渲染(浏览器优化可能每帧渲染一次)