浏览器+Node事件循环

642 阅读6分钟

解决的问题

jvascript是单线程,一个操作dom,一个是删除dom,就会冲突,处理事件循环,浏览器的时间循环机智,先按照先进先出原则执行宏任务,如果宏任务队列中出现对应的微任务,则执行完宏任务后会执行其对应的微任务,直到执行完成微任务再执行下一个宏任务,方便插队任务处理

为了让这些事件有条不紊地进行,JS引擎需要对之执行的顺序做一定的安排,V8 其实采用的是一种队列的方式来存储这些任务, 即先进来的先执行。模拟如下:

bool keep_running = true;
void MainTherad(){
  for(;;){
    //执行队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);
    
    //执行延迟队列中的任务
    ProcessDelayTask()

    if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
        break;}}

微、宏任务

  • 宏任务:I/O操作,整体代码、new promise、定时器
  • 微任务:MutaionObserver(前端的回溯)、promise.then、process.nextTick(node中)、Promise 为基础开发的其他技术(比如fetch API)、V8 的垃圾回收过程
  • Await相当于new promise await 以后得执行东西相当于放入then中
  1. 为什么引入微任务概念
  • 其实引入微任务的初衷是为了解决异步回调的问题。想一想,对于异步回调的处理,有多少种方式?总结起来有两点:
    1. 将异步回调进行宏任务队列的入队操作。
    2. 将异步回调放到当前宏任务的末尾。
      如果采用第一种方式,那么执行回调的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿
  • 宏任务是先进先出的原则,如果有比较紧急的任务出现,就需要插队,所以引入微任务的概念
  1. Node中事件循环和浏览器中的时间循环区别
  • 宏任务执行顺序:
  • 1.timers定时器,执行已经安排的settimeout 和setinterval函数
  • 2.Pending callback待定回调,执行延迟到下一个循环迭代的i/o回调
  • 3.idle prepare仅仅系统内部使用
  • 4.poll 检查新的i/o事件,执行与i/o相关
  • 5.check执行settimeout回调函数
  • 6.closr callback 执行socket.on的回调 Node v10以前是执行以上的阶段 再执行nextTick队列中的内容 再执行微任务队列的内容;Node v10以后宏任务和微任务浏览器完全一样

实例

console.log('start');
setTimeout(() => {
  console.log('timeout');
});
Promise.resolve().then(() => {
  console.log('resolve');
});
console.log('end');

答案

start
end
resolve  //作为promise微任务首先被注入队列
timeout
Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
});
setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0);
console.log('start');

答案:

// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
setTimeout(function(){
    console.log('1')
});
new Promise(function(resolve){
    console.log('2');
    resolve();
}).then(function(){
    console.log('3')
});
console.log('4');
new Promise(function(resolve){
    console.log('5');
    resolve();
}).then(function(){
    console.log('6')
});
setTimeout(function(){
    console.log('7')
});
function bar(){
    console.log('8')
    foo()
}
function foo(){
    console.log('9')
}
console.log('10')
bar()

答案:

解析 
首先浏览器执行Js代码由上至下顺序,遇到setTimeout,把setTimeout分发到宏任务Event Queuenew Promise属于主线程任务直接执行打印2 
Promis下的then方法属于微任务,把then分到微任务 Event Queueconsole.log(‘4’)属于主线程任务,直接执行打印4 
又遇到new Promise也是直接执行打印5Promise 下到then分发到微任务Event Queue中 
又遇到setTimouse也是直接分发到宏任务Event Queue中,等待执行 
console.log(‘10’)属于主线程任务直接执行 
遇到bar()函数调用,执行构造函数内到代码,打印8,在bar函数中调用foo函数,执行foo函数到中代码,打印9 
主线程中任务执行完后,就要执行分发到微任务Event Queue中代码,实行先进先出,所以依次打印36 
微任务Event Queue中代码执行完,就执行宏任务Event Queue中代码,也是先进先出,依次打印17。 
最终结果:24510893617

nodejs 和 浏览器的 eventLoop 还是有很大差别的

  1. 三大关键阶段

    1. 执行 定时器回调 的阶段。检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeout、setInterval。这个阶段暂且叫它timer
    2. 轮询(英文叫poll)阶段。因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?就是通过'data'、 'connect'等事件使得事件循环到达 poll 阶段。到达了这个阶段后: 如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到timer阶段。 如果没有定时器, 会去看回调函数队列。
    • 如果队列不为空,拿出队列中的方法依次执行

    • 如果队列为空,检查是否有 setImmdiate 的回调

    • 有则前往check阶段(下面会说)

    • 没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段

    1. check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。

2. 完善

首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的回调阶段。比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。

并且在 check 阶段结束后还会进入到 关闭事件的回调阶段。如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。

梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:

  1. timer 阶段
  2. I/O 异常回调阶段
  3. 空闲、预备状态(第2阶段结束,poll 未触发之前)
  4. poll 阶段
  5. check 阶段
  6. 关闭事件的回调阶段

是不是清晰了许多?

3. 实例演示

好,我们以上次的练习题来实践一把:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

首先说 node 版本 >= 11的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务

timer1
promise1
time2
promise2

而 node 版本小于 11 的情况下,对于定时器的处理是:

若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,那么就将微任务暂时保存,直接去执行新的定时器任务,当新的定时器任务执行完后,再一一执行中途产生的微任务

timer1
timer2
promise1
promise2

nodejs 和 浏览器关于eventLoop的主要区别

两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。

关于process.nextTick

process.nextTick 是一个独立于 eventLoop 的任务队列。 在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。