Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

node中的Event模块(上) #34

Open
SunShinewyf opened this issue Nov 9, 2017 · 7 comments
Open

node中的Event模块(上) #34

SunShinewyf opened this issue Nov 9, 2017 · 7 comments

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Nov 9, 2017

前言:最近对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事件基础的一些总结,有不正确的地方还望指出,共同学习。

@vidahaha
Copy link

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

@SunShinewyf
Copy link
Owner Author

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

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

@vidahaha
Copy link

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

@SunShinewyf
Copy link
Owner Author

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

@toBeTheLight
Copy link

你好,我有两个疑问。

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

@xtx1130
Copy link

xtx1130 commented Mar 7, 2018

@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的源码了解一下原理

@ProfutW
Copy link

ProfutW commented May 14, 2018

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

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

nextTick是最后输出的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants