本文是系列文章第二篇,介绍js的第二种宿主环境,即node.js,想了解本系列其他参考这里。
1 概述
node.js是一个开源和跨平台的js运行时环境(runtime environment,也被称为host environment),和chrome一样 内置 v8 js引擎,另外的宿主api为js提供了服务端工作的能力。
本文会对node.js的实现原理和常见用法做简要介绍。
2 实现方式
node.js的实现方式主要是封装了c++/c项目v8和libuv的代码,并结合一些其他library为开发者提供了js接口,具体依赖的library包括
- v8
- libuv
- llhttp
- c-ares
- OpenSSL
- zlib
注意在源码中有两个主要目录,其中lib包含所有我们使用的js模块,src中是下面介绍的各种以c++/c为主的依赖实现。
2.1 v8
v8是一个跨平台的js引擎,由c++编写,实现了ecma规范,其他常见js引擎还包括
- firefox的SpiderMonkey
- safari的JavaScriptCore
js通常被认为是一个解释型语言,但是现代js引擎为了加速运行会通过 just-in-time (JIT) 编译,虽然在编译js时花了一些事件,但是执行时会比单纯的解释性能更好。
js引擎会首先将js代码解析为ast,并进一步通过解释器解释为字节码(bytecode),字节码是一种通常与机器无关的中间代码,如果按传统方式便可以直接在js引擎运行。为了更快运行,字节码被发送到优化编译器,后者会将字节码优化为执行更快地机器码(machine code),如果编译优化出现错误会返回字节码,更多细节参考JavaScript engine fundamentals: Shapes and Inline Caches。
2.2 libuv
由c编写,设计的初衷是为node.js提供跨平台的事件驱动非阻塞异步i/o模型,提供的功能包括
- 支持epoll, kqueue, IOCP, event ports的全功能event loop
- 异步TCP and UDP sockets
- 异步dns解析
- 异步文件和文件系统操作
- 文件系统时间
- 子进程
- 线程池, 具体实现因系统而异,epoll on Linux, kqueue on OSX and other BSDs, event ports on SunOS and IOCP on Windows
- 线程和同步原语
其中阻塞指的是另外的js代码需要等待非js操作完成才能执行,i/o指的是与磁盘和网络之间的交互。
2.2.1 Handles and requests
libuv提供了两个和event loop一起使用的抽象Handles(中文翻译为句柄) and requests。
其中handle表示long-lived对象,可以在激活时处理一些特定的操作,比如
- 一个prepare handle会在每次event loop前都会获取它的回调
- 一个tcp server handle在每次有新连接时都会获取它的连接回调
request表示short-lived操作,这些操作可以被一个handle执行,比如write request可以被handle或单独用来写数据。
2.2.2 i/o loop
i/o loop,或被称为event loop,是libuv的核心部分,用单线程来执行,这部分会结合node.js实际使用的event loop来理解。
浏览器中的event loop在html标准中被定义,我们在这篇文章有过讨论,这里简要重复一下,在浏览器中的event loop中存在一个microtask queue和一个或多个task(或被称为macroTask) queues,每个macro task完成后会检查microtask queue,将包含的microtask及其生成的microtask执行结束,就执行必要的渲染,然后执行下一个macrotask。
在node.js中的event loop有所不同,包含一个microtask queue和各种phase,每个phase执行的是macrotask,每个macrotask结束后执行microtask queue中的microtask,直到microtask queue为空(在node.js@11 之前是执行完每个phase后再执行microtask,详情查看New Changes to the Timers and Microtasks in Node v11.0.0)。
其中microtask在node.js中包括process.nextTick()
和Promise相关回调.
具体每个event loop中的phase包括
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
具体为
- timers 本阶段如果有到期的timer(包括setTimeout 和 setInterval)会执行其回调,否则结束本阶段。
- Pending callbacks 执行上一轮loop被延迟到本次的回调,比如tcp错误
- ide,prepare node.js内部使用
- poll 轮询阶段,处理close回调、timer、setImmediate外所有异步i/o回调,主要是计算各个回调还有多久调用,如果可以调用了就被放在poll queue。
- 如果没有timer到期
- 如果poll queue非空,则会迭代这个queue同步执行,直到都被执行完或者到达系统相关的限制节点
- 如果poll queue空的,则
- 如果存在setImmediate()回调,则会到下一阶段执行该回调
- 否则,event loop会等待回调被加入poll queue然后立刻执行
- 一旦poll queue为空,就会检查timer,如果有timer到期,event loop会返回timer阶段执行会掉
- 如果没有timer到期
- check 执行setImmediate回调
- Close callbacks 执行close回调
注意下面这个案例
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
因为timer的最小时间为1(见timer文档),因此如果在1ms内执行了setImmediate就会先执行setImmediate,否则先执行setTimeout
其他参考
- JavaScript Event Loop vs Node JS Event Loop
- NodeJS 系列之事件循环
- Libuv design overview
2.2.3 File I/O
对于文件i/o,libuv没有依赖特定平台的原语,用的是一个线程池的i/o操作,该线程池是全局的,可以执行以下操作,
- file操作
- dns方法
- 用户特定的代码
2.3 llhttp
由c和ts编写,用来解析http,因为没有系统调用和分配,因此占用很小内存
2.4 c-ares
由c编写,用来处理异步的dns请求
2.5 OpenSSL
提供加密功能,用在tls and crypto模块
2.6 zlib
用来压缩和解压缩
3 常用模块
node.js借助上述底层依赖提供了丰富的模块和详实的api文档,下面我们挑选常用的几个进行介绍。
3.1 process
表示当前的进程,不需要require直接使用,可以从其获取一些信息,比如可以通过process.env.NODE_ENV
获取环境变量。
3.2 http
使用时需要require('http')
引入,可以用来做http server和client,一个简单的使用如
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
是我们最常用的模块之一
3.3 events
大部分的node.js 核心api都是基于异步的事件驱动架构,其中包含一类对象(即emitter)用来触发事件来调用相关监听器。比如
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');
和浏览器中的Event接口类似。
3.4 file
提供了和文件系统交互的方法
3.5 path
提供了处理文件和目的时路径相关的工具
3.6 module
这篇文章详细介绍了es module,在node.js中除此之外还有commonjs,两者的主要区别是前者对变量是live bindings,后者是浅拷贝(对原始类型复制值,对引用类型复制地址),因此前者对循环引用也能好的处理(如果出现循环引用,后者会只输出已经执行的部分,具体参考上面提到的链接)。
4 框架
node.js框架内容很多,基本功能是处理http连接。由于目前工作不涉及选型这一块,因此先丢两个参考链接
- The complete guide to Node.js frameworks
- 10 Best Nodejs Frameworks for Web Apps in 2021
这里只讨论使用的最多的express.js和koa。
4.1 express
express是一个内置了部分常用功能的框架,可以用来处理一些基本操作,比如路由、使用中间件、使用模板引擎和错误处理。
4.1.1 路由
路由决定了怎么响应一个client对特定endpoint(指一个uri和一个特定的http方法)的请求,每个路由可以有一个或多个handler方法,会在路由匹配到时被调用。用来响应各种http请求。
4.1.2 中间件
中间件(Middleware )指的是可以作为路由handler的函数,可以访问到req,res和next函数,其中req表示请求对象,res表示响应对象,通过next调用可以将控制权向下传递。本质就是在应用的req-res循环过程中执行的函数。
可以用来
- 执行任何代码
- 修改req,res对象
- 结束req-res循环
- 调用下一个中间价
4.1.3 模板引擎
模板引擎(template engine)用于将模板中的变量用实际数据表示,并将模板转换为html文件。
4.1.4 基本使用
express导出的是一个函数,并且挂载了多个静态方法。express源码入口文件如下
exports = module.exports = createApplication;
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
app.init();
return app;
}
/**
* Expose the prototypes.
*/
exports.application = proto;
exports.request = req;
exports.response = res;
/**
* Expose constructors.
*/
exports.Route = Route;
exports.Router = Router;
/**
* Expose middleware
*/
exports.json = bodyParser.json
exports.query = require('./middleware/query');
exports.raw = bodyParser.raw
exports.static = require('serve-static');
exports.text = bodyParser.text
exports.urlencoded = bodyParser.urlencoded
具体实现细节参考How express.js works。
导出的函数express调用后返回一个app对象,在这里表示application,包含各种方法,表示启用中间件、路由和模板引擎等。
express.Router()可以返回一个可以作为handler的router实例,比如
var express = require('express')
var router = express.Router()
// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
console.log('Time: ', Date.now())
next()
})
// define the home page route
router.get('/', function (req, res) {
res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
res.send('About birds')
})
module.exports = router
var birds = require('./birds')
// ...
app.use('/birds', birds)
express.static(root, [options])是一个内置中间件用来作为静态文件服务器,比如
app.use(express.static('public'))
我们可以使用以下路径访问public目录下的文件,注意参数中的文件夹本身并不在目录路径中
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html
其他参考官方文档
4.2 koa
koa是有express团队出的另一个框架,和express的区别主要是引入了async函数和没有内置任何中间件,利用async和next可以实现真正意义上的中间件,即express等框架只是利用中间件将控制权向下传递,而koa可以向下传递后,然后可以再返回第一个中间件,类似于dom中的捕获和冒泡,这部分可以参考官方说明
源码分析可以参考十分钟带你看完 KOA 源码
5 进程管理工具
进程管理工具(Process managers)用来管理node.js应用,可以保证用来在crash时重启、查看运行时性能和资源使用情况以及集群控制等,比如pm2。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!