js事件循环

20 阅读4分钟

最近换工作面试,面试官问了一个node的事件循环,我竟然发现我只留下一丁点记忆,明明自己还写过相关的博客,真的恍若隔世,自从我服用了帕罗西丁之后,记忆力确实已经大不如前了。为了加强记忆,我再重新写一遍吧。

node时间驱动理解

基于事件驱动:webServer收到请求,把它关闭后进行处理,然后去服务下一个web请求,当请求完成,结果集被放到处理队列,当到达队列列头,把结果集返回用户,因为webServer一直接受请求而不等待任何读写操作,所以被称为驱动io,也叫非阻塞io: image.png

生成一个主循环来监听事件,检测到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事件循环分六个阶段,每个阶段都可以认为是一个宏任务

  1. time阶段:处理timeout,interval回调,由poll阶段控制
  2. io callback阶段:处理系统级别回调,如io读写回调,tcp失败回调
  3. idle,prepare阶段:node内部使用,可忽略
  4. poll阶段:处理io操作回调,计算应该阻塞等待I/O的时间,如果poll队列空了,在有setImmediate的时候会进入check阶段,否则进入空闲阶段。
  5. 空闲阶段:暂停,等待新回调加入
  6. check阶段:执行setImmediate阶段
  7. close callback阶段:执行关闭回调,如socket.on('close',()=>{})等

image.png 在这流程基础上有几个奇怪的执行时机:

特殊阶段

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/finallyMutationObserverqueueMicrotask()和宏任务队列如setTimeoutsetIntervalsetImmediate(非标准)、requestAnimationFrameI/OUI渲染

具体的执行流程是:

  1. 执行同步代码---入调用栈
  2. 遇到异步API,交给WEBAPI处理,回调函数放入对应队列
  3. 同步代码执行完毕,调用栈清空
  4. 检查微任务队列,执行完所有微任务
  5. 在宏任务队列中执行第一个宏任务
  6. 重复步骤4-5(每次执行完宏任务后都清空微任务队列)
  7. 必要时进行UI渲染(浏览器优化可能每帧渲染一次)