-
Notifications
You must be signed in to change notification settings - Fork 20
Description
概述
NodeJS本身不是开发语言,它是一个工具或者平台,在服务器端解释、运行Javascript。NodeJS利用Google V8来高效率地解释运行Javascript,而Javascript做的只是调用这些API而已。NodeJS里的libuv为开发者提供了异步编程的能力。
libuv 采用了 异步 (asynchronous), 事件驱动 (event-driven)的编程风格, 其主要任务是为开人员提供了一套事件循环和基于I/O(或其他活动)通知的回调函数, libuv 提供了一套核心的工具集, 例如定时器, 非阻塞网络编程的支持, 异步访问文件系统, 子进程以及其他功能.
1. 模块化的本质
NodeJS 把每个JavaScript文件封装成一个模块,一个模块其实就是函数,因为函数本来就是一个执行上下文,可以通过node demo.js得到,这个函数的参数
# demo.js
console.log(arguments);
# output:
{ '0': {},
'1':
{ [Function: require]
resolve: { [Function: resolve] paths: [Function: paths] },
main:
Module {
id: '.',
exports: {},
parent: null,
filename: 'E:\\myWorks\\Workbench\\web\\modu.js',
loaded: false,
children: [],
paths: [Array] },
extensions: { '.js': [Function], '.json': [Function], '.node': [Function] },
cache: { 'E:\myWorks\Workbench\web\modu.js': [Object] } },
'2':
Module {
id: '.',
exports: {},
parent: null,
filename: 'E:\\myWorks\\Workbench\\web\\modu.js',
loaded: false,
children: [],
paths:
[ 'E:\\myWorks\\Workbench\\web\\node_modules',
'E:\\myWorks\\Workbench\\node_modules',
'E:\\myWorks\\node_modules',
'E:\\node_modules' ] },
'3': 'E:\\myWorks\\Workbench\\web\\modu.js',
'4': 'E:\\myWorks\\Workbench\\web' }
从上图可以看到,每个JS文件之所以可以访问module、exports、require()、__filename、__dirname,就是因为NodeJS把我们写的JS文件封装成一个模块,这个模块就是一个函数执行上下文,而函数的入参就有它们。
我们还可以通过global对象访问全局对象。
A. 模块的分类:
NodeJS 里的模块分为两种:
- 核心模块,系统自带的模块,安装NodeJS就已经带上了
- 文件模块,包括第三方模块(通过指令
npm和yarn引入的其他人写好的模块)和自己编写的模块
B. 访问主模块:
当 Node.js 直接运行一个文件时,require.main 会被设为它的 module。 这意味着可以通过 require.main === module 来判断一个文件是否被直接运行:
对于 foo.js 文件,如果通过 node foo.js 运行则为 true,但如果通过 require('./foo') 运行则为 false。
C. 模块解析:
1. 区别模块类型
当没有以 '/'、'./' 或 '../' 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。
2. 填充后缀
如果按确切的文件名没有找到模块,则 Node.js 会尝试带上 .js、.json 或 .node 拓展名再加载。
但是文件名被解析成一个目录,如果目录下有package.json,入口文件将会是main字段指定的文件;如果目录下没有package.json,Node.js 就会试图加载目录下的 index.js 或 index.node 文件。
3. 填充路径
如果传递给 require() 的模块标识符不是一个核心模块,也没有以 '/' 、 '../' 或 './' 开头,则 Node.js 会从当前模块的父目录开始,尝试从它的 /node_modules 目录里加载模块。 Node.js 不会附加 node_modules 到一个已经以 node_modules 结尾的路径上。
4. 全局目录
如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。
5. 查找失败
如果给定的路径不存在,则 require() 会抛出一个 code 属性为 'MODULE_NOT_FOUND' 的 Error。
D. 模块缓存:
模块在第一次加载后会被缓存。 这也意味着(类似其他缓存机制)如果每次调用 require('foo') 都解析到同一文件,则返回相同的对象。
多次调用 require(foo) 不会导致模块的代码被执行多次。 这是一个重要的特性。 借助它, 可以返回“部分完成”的对象,从而允许加载依赖的依赖, 即使它们会导致循环依赖。
模块是基于其解析的文件名进行缓存的。 在不区分大小写的文件系统或操作系统中,被解析成不同的文件名可以指向同一文件,但缓存仍然会将它们视为不同的模块,并多次重新加载。
E. 核心模块:
Node.js 有些模块会被编译成二进制。require()总是会优先加载核心模块。 例如,require('http') 始终返回内置的 HTTP 模块,即使有同名文件。
F. 循环依赖:
当循环调用 require() 时,一个模块可能在未完成执行时被返回。
2. 包管理
NodeJS项目里可以通过NPM和package.json管理第三方包。
NPM
NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题。允许用户从NPM服务器下载别人编写的第三方包或命令行程序到本地使用,也允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。
Package.json
name - 包名。
version - 包的版本号。
description - 包的描述。
homepage - 包的官网 url 。
author - 包的作者姓名。
contributors - 包的其他贡献者姓名。
dependencies - 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下。
repository - 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上。
main - main 字段指定了程序的主入口文件,require('moduleName') 就会加载这个文件。这个字段的默认值是模块根目录下面的 index.js。
keywords - 关键字.
发布自己的模块
首先使用npm adduser指令创建NPM账户,或者使用npm login指令登录NPM账号,然后创建自己的库,package.json是必须的,用于描述模块,使用npm publish指令发布出去。
3. 事件循环
下面是官方给出的NodeJS的事件循环示意图:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
事件循环可以说是NodeJS的核心,作为web服务器,可以把接收到的请求放到事件队列中去,并把阻塞的操作放到异步模块中,这样一来可以高效率使用cpu,提高用户响应。
1. 关键的API
setTimeout()、setInterval()属于timers阶段的。
setImmediate()属于check阶段。
process.nextTick()将 callback 添加到"next tick 队列",在micro-task-queue被调用之前执行。递归调用nextTick callbacks 会阻塞任何I/O操作,就像一个while(true); 循环一样。
Promise.then()属于micro-task-queue。
2. 与浏览器事件循环机制的不同
NodeJS和浏览器的事件循环机制不一样。
浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
3. 关键阶段
- timers,执行由timer库产生的回调。
- poll,NodeJS内置的很多异步API的回调都是在poll阶段执行的。比如fs.read之类的。
- check,执行
setImmediate()产生的回调。
参考
[译] 你不知道的 Node
深入理解js事件循环机制(Node.js篇)
The Node.js Event Loop, Timers, and process.nextTick()
【Node.js】理解事件循环机制
Node.js机制及原理理解初步
module - 模块
