issue-blog icon indicating copy to clipboard operation
issue-blog copied to clipboard

node中的Event模块(上)

Open SunShinewyf opened this issue 6 years ago • 7 comments

前言:最近对node底层一些东西不是很深入,趁这段时间整理一些理论知识

js中的事件循环(Event Loop)

Event Loop是指在js执行环境中存在主执行线程和任务队列(Task Queue),其中所有同步任务都在主执行线程中形成一个执行栈,所有异步任务都会放到任务队列中。Event Loop会经历如下过程:

  • 主线程执行同步任务,在主线程执行过程中,不断形成堆栈并执行出栈入栈的操作
  • 主线程任务是否执行完毕,如否,继续循环第1步,如是,则执行下一步
  • 系统读取任务队列里的任务,进入执行栈,开始执行
  • 不断循环执行前三步

参考资料:

macrotaskmicrotask

上面说的异步任务中,分为macrotask(宏任务)和microtask(微任务)两类,在挂起任务中,Js引擎会按照类别将任务分别存放在这两种类型任务中。这两种任务执行的顺序如下:

  • 先取出macrotask任务队列中的第一个任务进行执行
  • 执行完毕后取出microtask中的所有任务顺序执行
  • 再取macrotask中的剩余任务执行
  • 重复前面三个步骤

这个步骤通过一个图来展示会比较直观:

images

图中stack表示主执行线程中的同步任务,而Background Threads则是指macrotask,在执行完主线程之后,会取出Macrotask Queue(也叫Task Queue)中的第一个任务setInterval执行,执行完毕之后就会顺序执行下面的Microtask Queue,直到所有Microtask Queue中的任务都执行完毕了之后,才会执行下一个Macrotask

其中macrotask类型包括:

  • script整体代码
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

microtask类型包括:

  • process.nextTick
  • Promise(这里指浏览器实现的原生promise)
  • Object.observe
  • MutaionObserver

参考资料:

通过一段代码来验证一下上面的理论:

console.log('start')

setTimeout(() => {
console.log('setTimeout1');
},0);
 
const myInterval = setInterval(() => {
    console.log('setInterval');
},0)

setTimeout(() => {
    console.log('setTimeout2');
    Promise.resolve().then(() => {
        console.log('promise3');
    })

    setTimeout(() => {
        console.log('setTimeout3');
        clearInterval(myInterval);
    },0)
},0)

Promise.resolve()
        .then(() => {
            console.log('promise1');
        }).then(() => {
            console.log('promise2');
        })
console.log('end');

这段代码最后的输出结果如下:

start
end
promise1
promise2
setTimeout1
setInterval
setTimeout2
promise3
setInterval
setTimeout3

大概讲解一下流程:

  • 首先整段script相当于一个Macrotask,它是Macrotask Queue中的第一个任务,先执行,所以打印出 startend
  • Promise相当于一个Microtask,按照之前的理论,会先顺序执行完所有的Microtask,所以此时会打印promise1promise2
  • 执行完所有的Microtask之后,会将setTimeout1setInterval推进Macrotask Queue中,并且会执行此时Macrotask Queue的第一个任务,也就是setTimeout1,此时打印出setTimeout1
  • 而此时Microtask还是为空,所以会继续执行下一个Macrotask,也就是setInterval,此时打印出setInterval
  • 在执行setIntervaltask时,会将下一个setTimeout继续推进Macrotask Queue,而且此时Microtask仍然为空,继续执行下一个Macrotask,所以打印出setTimeout2
  • 在执行完setTimeout2的时候,setTimeout2里面的promise已经推进Microtask Queue中,所以此时会执行完Microtask Queue中的任务,打印出promise3
  • 在执行Microtask Queue的时候,一直执行的setInterval后面的setTimeout3会继续被推进Macrotask Queue中,并且依次执行,直到setInterval被取消。

node中的Event Loop

根据node官方文档的描述,node中的Event Loop主要有如下几个阶段:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

各个阶段执行的任务如下:

  • timers 阶段: 这个阶段执行setTimeoutsetInterval预定的callback;
  • I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers设定的callbackssetImmediate()设定的callbacks这些之外的callbacks;
  • idle, prepare 阶段: 仅node内部使用;
  • poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
  • check 阶段: 执行setImmediate() 设定的callbacks;
  • close callbacks 阶段: 执行socket.on('close', ...)这些 callback

process.nextTick()

process.nextTick()并没有在Event Loop的执行阶段中,而是在Event Loop两个阶段之间运行,根据上面说的,process.nextTick()属于microtask任务类型。

根据process.nextTick()的运行性质,可以整理出下面的简图:

images

也就是process.nextTick()有可能插入在Event Loop各个阶段中

setTimeout(fn,0) Vs setImmediate Vs process.nextTick()

setTimeout(fn,0) Vs setImmediate

  • setTimeout(fn,0)timer阶段执行,并且是在poll阶段进行判断是否达到指定的time时间才会执行
  • setImmediatecheck阶段才会执行

两者的执行顺序要根据当前的执行环境才能确定,根据官方文档总结得出的结论是:

  • 如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,即随机。
  • 如果两者都不在主模块调用(即在一个 IO circle 中调用),那么setImmediate的回调永远先执行。

setImmediate Vs process.nextTick()

  • setImmediate()属于check观察者,其设置的回调函数,会插入到下次事件循环的末尾,每次事件循环只执行链表中的一个回调函数。
  • process.nextTick()所设置的回调函数会存放到数组中,一次性执行所有回调函数。
  • process.nextTick()调用深度的限制,上限是1000,而setImmediate没有;

先来看一段代码:

setImmediate(() => console.log('immediate1'));
setImmediate(() => console.log('immediate2'));

setTimeout(() => console.log('setTimeout1'), 1000);
setTimeout(() => {
    console.log('setTimeout2');
    process.nextTick(() => console.log('nextTick1'));
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);

process.nextTick(() => console.log('nextTick2'));
process.nextTick(() => {
    process.nextTick(console.log.bind(console, 'nextTick3'));
});
process.nextTick(() => console.log('nextTick4'));

在控制台中执行node index.js,得到的结果如下:

nextTick2
nextTick4
nextTick3
setTimeout2
setTimeout3
nextTick1
immediate1
immediate2
setTimeout1

分析如下:

  • node中,nextTick的优先级高于setTimeoutsetImmediate(),所以会先执行nextTick里面的信息打印。
  • 但是对于嵌套的nextTick,会慢于同步的nextTick,所以nextTick4会先于nextTick3
  • 然后开始一个Event Loop过程,首先执行timer阶段,而此时setTimeout所需要等待的时间是0,所以立即执行setTimeout2setTimeout3里面的逻辑。而setTimeout1由于设置了执行时间,不满足执行条件,被放到下一轮Event Loop
  • 当前Event Loop执行到check阶段,于是打印出immediate1immediate2
  • 执行后面的Event Loop,当setTimeout1达到执行条件时执行

参考资料: -Node.js的event loop及timer/setImmediate/nextTick -Node.js Event Loop 的理解 Timers,process.nextTick()

node事件基础的一些总结,有不正确的地方还望指出,共同学习。

SunShinewyf avatar Nov 09 '17 10:11 SunShinewyf

学姐 setTimeout(fn,0) Vs setImmediate 到底是随机的还是setImmediate在后面呢? 我这里的结果有点晕。。 qq 20171115205946

vidahaha avatar Nov 15 '17 13:11 vidahaha

setTimeout(fn,0) Vs setImmediate的执行顺序是随机的,具体要看两者是否在一个I/O循环中进行调用,如果在,setImmediate 始终会比 setTimeout 先执行。因为setImmediate 会在 event looppoll 完成之后立即执行,setTimeout 则是到下一个 timers 阶段。

还有,你后面的打印结果是我上面的例子吗

SunShinewyf avatar Nov 16 '17 10:11 SunShinewyf

嗯 懂了 这个结果是你上面的例子

vidahaha avatar Nov 17 '17 02:11 vidahaha

因为setTimeout(fn,0) Vs setImmediate的结果可能是随机的,所以就会出现有的时候setImmediatesetTimeout(fn,0)之前执行,有的时候在setTimeout(fn,0)之后执行

SunShinewyf avatar Nov 17 '17 02:11 SunShinewyf

你好,我有两个疑问。

  1. node和浏览器的不同。在循环过程中,node中每个阶段的任务是一次性拿出,执行完毕后再清空执行microtask/nextTick,再进行下个阶段,而浏览器是执行一个macrotask,就执行清空microtask/nextTick,再进行下个macrotask任务,是这样吗?
  2. 比如说我在timer阶段执行setTimeout过程中创建的setTimeout是进入下轮loop吗,还是直接放入当前阶段?(我知道如果执行timer时创建了check,肯定会优先放入当前loop,有这个疑问主要是写了很多测试代码,不知道是不是哪里搞错了,过一会执行结果就变了)

toBeTheLight avatar Mar 07 '18 08:03 toBeTheLight

@toBeTheLight 正好watch这个blog,看到了你的问题,第二个问题我可以回答你: setTimeout(fn,time) 这里面的fn是以相同的time为基准,把fn存储到一个双向链表中。当:

setTimeout(() => setTimeout(fn,0),0)

在运行event-loop的timers阶段的时候:
外层的setTimeout会执行此时刻链表里面的所有fn,而只有在执行到外层的fn(即() => setTimeout(fn,0))的时候,才能把内部的fn注册到setTimeout中,所以会移到下一个event-loop的timers阶段运行。但是如果内层是setImmediate的时候,会把setImmediate注册到check阶段,而此轮event-loop的check阶段尚未运行,所以会放到此轮的event-loop中运行。有兴趣的话可以看一下timer.js的源码了解一下原理

xtx1130 avatar Mar 07 '18 11:03 xtx1130

setImmediate属于check观察者没错,但是并不是每次只执行链表中的一个回调函数,而是一次取出全部执行完:

setImmediate(() => {
    console.log('setImmediate1');
    process.nextTick(() => {
        console.log('nextTick');
    });
});
setImmediate(() => {
    console.log('setImmediate2');
});
setImmediate(() => {
    console.log('setImmediate3');
});

nextTick是最后输出的。

ProfutW avatar May 14 '18 13:05 ProfutW