NodeJs(一)

Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications.

Node简介

Node是JavaScript语言的服务器运行环境,是一个基于Chrome V8引擎的JavaScript运行时。

所谓“运行环境”有两层意思:

  • JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机的意思。
  • Node提供大量工具库,使得JavaScript语言与操作系统互动(例如读写文件、新建子进程),在一层Node又是JavaScript的工具库。

Node的特点

异步I/O

对于异步的I/O这个概念,对于用过Ajax的人来说,要理解起来再简单不过了。我们通过发起一个Ajax请求:

$.post('/url', {title: 'test.js'}, function (data) { 
console.log('收到响应'); 
}); 
console.log('发送Ajax请求结束');
// 发送Ajax请求结束 => 收到响应
复制代码

我们在调用$.post()后,后续代码是被立即执行的,但“收到响应”的执行时间不可预估的。如下图:

image.png

在Node中,异步I/O也是差不多的原理。我们以文件读取为例:

cosnt fs = require('fs')
fs.readFile('/path', (err, file) => {
  console.log('读取文件完成')
})
console.log('发起读取文件')
// 发起读取文件 => 读取文件完成
复制代码

image.png

I/O是耗时昂贵的,分布式I/O更加昂贵。加载一个资源来自两个不同位置的数据返回结果,第一个资源需要M毫秒耗时,第二个资源需要N毫秒。如果采用同步的方式:

getData('from_db'); // 消费时间为M
getData('form_remote_api') // 消费时间为N
复制代码

如果我们采用异步的方式,第一个资源的获取并不会阻塞第二个资源,也即第二个资源的请求并不会依赖第一个资源的结束,因此我们可以享受到并行的优势:

getData('form_db', (req, res) => {
   // 消费时间为M
});
getData('form_remote_api', (req, res) => {
   // 消费时间为N
})
复制代码

二者的时间总消耗,前者为M+N, 后者为max(M + N)。随着应用复杂性的提高,同步与异步的优劣势将会逐渐凸显出来。

关于整个Node的异步I/O流程如下:

image.png

事件驱动/事件循环

img

  • 每个Node进程只有一个主线程在执行代码,形成一个执行栈(execution context stack)。
  • 除主线程之外,还维护了一个“事件队列(Event Queue)”。当网络请求和其他的异步操作进来时,Node都会放入Event Queue中,但是并不回立即执行,因此代码也并不会被阻塞,继续运行后面的代码,直到主线程执行完毕。
  • 再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。
  • 主线程不断重复步骤三,知道执行栈全部消费完。

每次事件循环都包含了6个阶段。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
复制代码
  • timers 阶段:这个阶段执行timer(setTimeoutsetInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行setImmediate()的回调
  • close callbacks 阶段:执行socketclose事件回调。

回调函数

除了异步和事件之外,回调函数也是它的一大特点,回调函数也是最好接受异步调用返回的数据的方式之一。但是这种编程方式对于很多习惯同步思路编程的人来说,是特别不习惯的。当我们的业务逻辑复杂的时候,回调的嵌套过多,代码复杂度增加,可读性降低,维护起来也复杂,调试也复杂,这个就是回调地狱。

function doByCallback(id, next) {
  startJobA(id, (err, resA) => {
    startJobB(id, (err, resB) => {
      startJobC(id, (err, resC) => {
      	startJobD(id, (err, resD) => {
          if (err) return next(err);
          return next(null, resD);
        })
      })
    })
  })
}
复制代码

当然现在我们很多方法的可以解决这个问题,例如async,then.js,generator等等。

const methodAsync = async (id, next) => {
  try {
    const resA = await startJobA(id);
 	  const resB = await startJobB(id);
    ...
  } catch (err) {
    return next(err);
  }
}
  
function *methodGenerator(id, next) {
  try {
    const resA = yield startJobA(id);
 	  const resB = yield startJobB(id);
    ...
  } catch (err) {
    return next(err);
  }
}  
复制代码

单线程

JavaScript是单线程的,在Node中保持了这个特点。并且在Node中,JavaScript与其余线程是无法共享任何状态的。单线程的最大好处就是不用像多线程那样处处留意状态的同步问题,没有死锁,没有上下文交换的性能开销。Node将所有阻塞操作都交给了内部的线程池去实现,而自身只负责不断的往返调度,并没有真正的I/O操作,从而实现异步非阻塞I/O。

但是单线程也有自身的弱点:

  • 无法利用多核CPU
  • 错误可能会引起整个应用退出,健壮性堪忧
  • 大量计算占用CPU导致无法继续调用异步I/O

但是后来HTML5定制了Web Workers的标准。Web Workers能够创建工作线程来进行计算,来解决JavaScript大量计算阻塞UI渲染的问题。

Node与操作系统的交互

从Node整个库的设计架构来看,Node大致被分为三层结构:

img

  • Node standard library层,是由JavaScript编写的,即是我们能够直接调用到的API,我们可以在源码中的lib目录下看到。
  • No bindings,这一层是JavaScript与底层C/C++沟通的关键,上层通过bindings调用下层,相互交换数据。
  • 最后这一次基建层,是支撑Node.js运行的关键,同样也是由C/C++编写。
    • V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript的关键,它为 Javascript提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
    • Libuv:为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。
    • C-ares:提供了异步处理 DNS 相关的能力。
    • ….

我们通过一个例子来简单了解一下Node与操作系统的一个交互逻辑:

const fs = require('fs');
fs.open('./test.json', 'w', (err, fd) => {
  // do something
});
复制代码

上面是一个简单的读取本地test.json文件的例子,整个调用流程是这样一个逻辑:

lib/fs.js → src/node_file.cc → uv_fs

async function open(path, flags, mode) {
    mode = modeNum(mode, 0o666);  
    path = getPathFromURL(path); 
    validatePath(path);  
    validateUint32(mode, 'mode'); 
    return new FileHandle(
        await binding.openFileHandle(pathModule.toNamespacedPath(path),          
        stringToFlags(flags),             
        mode, 
        kUsePromises
    ));
}
复制代码

node_file.cc

static void Open(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  const int argc = args.Length();
  if (req_wrap_async != nullptr) {  // open(path, flags, mode, req)
    AsyncCall(env, req_wrap_async, args, "open", UTF8, AfterInteger,
              uv_fs_open, *path, flags, mode);
  } else {  // open(path, flags, mode, undefined, ctx)
    CHECK_EQ(argc, 5);
    FSReqWrapSync req_wrap_sync;
    FS_SYNC_TRACE_BEGIN(open);
    int result = SyncCall(env, args[4], &req_wrap_sync, "open",
                          uv_fs_open, *path, flags, mode);
    FS_SYNC_TRACE_END(open);
    args.GetReturnValue().Set(result);
  }
}
复制代码

uv_fs

dstfd = uv_fs_open(NULL,
                     &fs_req,
                     req->new_path,
                     dst_flags,
                     statsbuf.st_mode,
                     NULL);
  uv_fs_req_cleanup(&fs_req);
复制代码

img

包与NMP

Node组织了自身的核心模块,同时也让第三方文件模块可以有序的编写和使用。但是由于在第三方模块中,模块与模块之间的仍然是散列分布的,相互之间无法直接应用。而在模块之外,包和NPM则是将模块联系在一起的一种机制。

image.png

包结构

├── package.json             # 包的描述文件
├── bin                      # 用于存放JavaScript代码的目录
├── lib                      # 用于存放JavaScript代码的目录
├── src                      # 用于存放JavaScript代码的目录
├── doc                    	 # 用于存放文档的目录
└── test                     # 用于存放单元测试用例的目录
复制代码

包描述文件与NPM

包描述文件主要用于表达非代码相关的信息。他是一个JSON格式的文件,一般位于项目的根目录下,只包的必要组成部分。在CommonJS中package.json文件定义了如下一些必须的字段。

{	
    "main": "index.ts", // 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用,模块引入方法require()在引入包时,会有限检查这个字段,并将其作为包中其余模块的入口 
    "module": "index.ts", // 定义 npm 包的ESM规范的入口文件,browser 环境和 node 环境均可使用	
    "scripts": {}, // 脚本对象说明	
    "dependencies": {}, // 使用当前包所需要依赖的包列表。 
    "devDependencies": {}, // 只在开发时需要依赖。
}
复制代码

NPM包安装命令

npm install xxxx --save 
npm install xxxx --save-dev
npm install xxxx -g
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享