Skip to content

NodeJS 运行原理 #43

@coconilu

Description

@coconilu

概述

NodeJS本身不是开发语言,它是一个工具或者平台,在服务器端解释、运行Javascript。NodeJS利用Google V8来高效率地解释运行Javascript,而Javascript做的只是调用这些API而已。NodeJS里的libuv为开发者提供了异步编程的能力。

libuv 采用了 异步 (asynchronous), 事件驱动 (event-driven)的编程风格, 其主要任务是为开人员提供了一套事件循环和基于I/O(或其他活动)通知的回调函数, libuv 提供了一套核心的工具集, 例如定时器, 非阻塞网络编程的支持, 异步访问文件系统, 子进程以及其他功能.

node

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文件之所以可以访问moduleexportsrequire()__filename__dirname,就是因为NodeJS把我们写的JS文件封装成一个模块,这个模块就是一个函数执行上下文,而函数的入参就有它们。

我们还可以通过global对象访问全局对象。

A. 模块的分类:

NodeJS 里的模块分为两种:

  1. 核心模块,系统自带的模块,安装NodeJS就已经带上了
  2. 文件模块,包括第三方模块(通过指令npmyarn引入的其他人写好的模块)和自己编写的模块

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项目里可以通过NPMpackage.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. 关键阶段

  1. timers,执行由timer库产生的回调。
  2. poll,NodeJS内置的很多异步API的回调都是在poll阶段执行的。比如fs.read之类的。
  3. check,执行setImmediate()产生的回调。

参考

[译] 你不知道的 Node
深入理解js事件循环机制(Node.js篇)
The Node.js Event Loop, Timers, and process.nextTick()
【Node.js】理解事件循环机制
Node.js机制及原理理解初步
module - 模块

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions