上一次我们讲了一下JavaScript的运行机制,这次我们来谈谈nodejs的运行机制,相信有了js的基础,关于nodejs 的运行机制我们可以很清楚的理解了,如果有不了解js运行机制的,可以看我的上一篇文章
https://mp.csdn.net/postedit/90691003
一、Nodejs的架构
首先上张图
这张图是从github下扒下来的,首先我们可以看到,nodejs的架构是分为三层的。
第一层就是我们的用户代码,用户代码即我们编写的应用程序代码、npm包、nodejs内置的js模块等,我们日常工作中的大部分时间都是编写这个层面的代码。
第二层是胶水代码或第三方插件层,能够让js调用c/c++的代码。我们可以将其理解为一个桥,桥这头是js,另一边是c或者c++,通过这个桥可以让js调用c或者c++。第三方插件是我们自己实现的c或c++库,同时需要我们自己实现胶水代码,将js和c/c++链接
第三层就是底层库。nodejs的 依赖库,有v8、linuv......
v8是google开发的一套高效的js引擎,nodejs能够高效执行js的代码的很大的原因就在它。
libuv:是用c语言实现的一套异步功能库,nodejs高效的异步编程模型很大程度上归功于libuv的实现
还有一些其他的依赖库:http-parser,npm......
libuv架构
由上面的内容,我们已经知道,nodejs实现异步编程的核心就是libuv,libuv承担着nodejs与文件,网络等异步任务的沟通桥梁。
首先我们来谈谈nodejs中的异步事件
- 非I/O:
- 定时器(setTimeout,setInterval)
- mircroTask(promise.then)
- process.nextTick()
- setImmediate
- DNS.lookup
- I/O
- 网络
- 文件
- DNS操作
文件I/O、异步DNS操作
libuv内部还维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操作,DNS操作,用于异步代码。每当js层传递
给libuv一个操作任务时,libuv就会把这个任务添加到队列中,之后分为两种情况
- 线程池内的线程都被占用的时候,队列中任务就要进行排队等待空闲线程
- 线程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还给线程池,等待下一个任务到来,同时以时间的方式通知event-loop,event-loop接收到事件后,执行该事件的回调函数
notes:对于4个线程的线程池,我们可以在nodejs启动的时候,设置环境变量UV_THREADPOOL_SIZE来调整,但是最大不能超过128个
MacroTask和MicroTask
首先在介绍微任务队列和宏任务之前,我们首先介绍一下nodejs中独有的processs.nextTick(),它并不属于liuv。nodejs中要处理的回调,除了libuv中要处理的之外,在nodejs中还有另外两个queue,第一个是nextTick Queue,专门用于存储process.nextTick(),第二个是MicroTask Queue,包含一些microTask任务。
接下来到我们的重点:nodejs的时间循环
在EventLoop完成一个阶段,到下一个阶段之前,Event Loop将会执行这nextTick Queue以及microTask queue里面的回调,直到这两个队列为空,一旦这两个队列为空之后,就会进入下一个阶段。同时在nextTick Queue和microTask Queue之间是有一个执行的先后顺序的,nextTick queue会先于MicroTask queue执行的
事件循环的原理
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────── ─┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming. │
│ │ poll │<─┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴─────────── ┐
└ ┤ close callbacks │
└───────────────────────┘
- node的初始化
- 初始化node环境
- 执行输入代码
- 执行process.nextTick回调
- 执行微任务队列
- 进入event-loop
- 进入timers阶段(执行setTimeout和SetInterval)
- 检查timer队列是否有到期的timer回调,如果有,将到期的timer回调按照timerid升序进行
- 检查是否有process.nextTick任务,如果有全部执行
- 检查是否有microtask,有全部执行
- 退出该阶段
- 进入IO阶段
- 检查是否有pending的io回调,如果有,执行回调,如果没有,退出该阶段
- 检查是否有process.nextTick()任务,如果有,全部执行
- 检查是否有MicroTask,如果有全部执行
- 退出该阶段
- 进入idle,prepare阶段
- 进入poll阶段
- 首先检查是否存在尚未完成的回调,存在,则
- 1.如果 有可用回调,
- 那么执行
- 检查是否有process.nextTick()回调,有全部执行
- 检查是否有MicroTask,如果有全部执行
- 退出该阶段
- 2.如果没有可用回调
- 检查是否有immediate回调,如果有退出poll阶段,如果没有,阻塞在此阶段,等待新的事件通知
- 1.如果 有可用回调,
- 如果不存在尚未完成的回调,退出poll阶段
- 首先检查是否存在尚未完成的回调,存在,则
- 进入check阶段
- 如果有immediate回调,执行所有回调
- 检查是否有process.nextTick()回调,如果有,全部执行
- 检查是否有MicroTask,如果有全部执行
- 退出该阶段
- 进入closing阶段
-
如果有immediate回调,则执行所有immediate回调。
-
检查是否有 process.nextTick 回调,如果有,全部执行。
-
检查是否有 microtaks,如果有,全部执行。
-
退出 closing 阶段
-
-
检查是否有活跃的handles(定时器、io等事件)
-
如果有,继续下一轮循环
-
没有则结束事件循环,退出程序
-
- 进入timers阶段(执行setTimeout和SetInterval)
以上就是nodejs中代码的执行过程,接下来我们上一段代码来检测我们学习的怎么样吧
Promise.resolve().then(() => {
console.log('resolve1');
});
process.nextTick(function() {
console.log('tick1');
process.nextTick(function() {
console.log('tick2');
});
process.nextTick(function() {
console.log('tick3');
});
});
Promise.resolve().then(() => {
console.log('resolve2');
});
process.nextTick(function() {
console.log('tick4');
});
Promise.resolve().then(() => {
console.log('resolve3');
});
process.nextTick(function() {
console.log('tick5');
});
答案在下方,各位小伙伴可以先思考一下
tick1
tick4
tick5
tick2
tick3
resolve1
resolve2
resolve3