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

NodeJS 运行原理 #43

Open
coconilu opened this issue Aug 7, 2018 · 0 comments
Open

NodeJS 运行原理 #43

coconilu opened this issue Aug 7, 2018 · 0 comments

Comments

@coconilu
Copy link
Owner

coconilu commented Aug 7, 2018

概述

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 - 模块

@coconilu coconilu changed the title NodeJS 事件循环 NodeJS 运行原理 Aug 11, 2018
@coconilu coconilu mentioned this issue Jun 23, 2020
68 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant