node 的基本概念和使用

219 阅读8分钟

Node 是什么

Node.js 是一个基于 Chrome V8 引擎的 JavaScript **运行环境(runtime)**s, Node 不是一门语言是让 js 运行在后端的运行时, 并且不包括 javascript 全集, 因为在服务端中不包含 DOM 和 BOM, Node 也提供了一些新的模块例如 http, fs 模块等。Node.js 使用了事件驱动、非阻塞式 I/O 的模型,使其轻量又高效并且 Node.js 的包管理器 npm,是全球最大的开源库生态系统。事件驱动与非阻塞 IO 后面我们会一一介绍。到此我们已经对 node 有了简单的概念。

node 可以理解成 ECMAScript 大部分语法 + 内置模块,而且提供了一个包管理器。内部采用的是多线程(比如文件读写,内部还是基于多线程,我们称之为为非阻塞异步 I/O),不过主线程还是单线程的,怎么做到的非阻塞呢,其实还是基于事件驱动。

Node 对比传统后端语言

我们时常听说,Node 适合处理 i/o 密集型的场景,不适合处理 cpu 密集型的场景,我的内心独白就是:讲人话。

传统语言是什么样的

传统语言,比如 java、python等,是多线程同步的,也就是说,一旦我有一个请求发送到服务器,服务器就会分配一个线程给我,如下图:

特点:

  1. 每个操作会分配一个线程,线程会等待该操作完成后,才重新释放回线程池。
  2. 如果多个线程访问同一个资源(操作同一个文件),比如线程1 想让 A 厨师给炒个西红柿鸡蛋,线程2 也想让 A 厨师给排个黄瓜,这时候,就会有锁的概念,A 厨师炒完西红柿鸡蛋,再给线程2 拍黄瓜。

缺陷:多个线程看起来貌似是并行处理,实际上是不停的切换时间片去执行,比如线程1 执行 50%,跳到线程2 执行 50%,再跳到线程3执行 50%,然后回到线程1,这样反复的轮流加载,其本身是同步的,存在阻塞,并且线程池为空时,更是直接阻塞,所以高并发场景下的性能受限。

node 是什么样的

Node中,是单线程非阻塞异步的,也就是说,一旦我有多个请求发送到服务器,服务器使用的是同一个线程来处理,如下图:

  1. 每个操作使用同一个线程,而且线程不会等待异步操作的结果,而是基于事件回调来处理异步。
  2. Node 是单线程,没有锁的概念。

Node 的应用场景和优势劣势

1. 基于 Node 单线程非阻塞的特点,所以它非常适合异步 I/O 密集型的高并发场景,哪怕瞬间来 1000 次 I/0 操作,线程会依次处理,因为不用等待结果,所以不会阻塞,这也是其适合高并发场景的原因。

2. 如果单次操作,cpu 占用时间特别长(比如高时间复杂度的计算,或压缩、加密等同步任务),这时候单线程就会成为 Node 的效能瓶颈,Node 处理的效率是比较低的,所以说 Node 不适合 cpu 密集型的场景。

3. go 的横空出世,node 高并发的优势也不存在了,但是 node 有个天然的优势,它能解析 Js 语法,现在我们做服务端渲染,一般都用 Node,天生的去支持 vue,react 语法。

4. BFF,服务于前端的后端语言,前端拿 Node 做中间层来解决跨域问题

并发和并行

乍一听,二者好像都一样,其实有着本质的区别。

  1. 并发是指,操作同时到达到服务端,但是服务端只能依次处理。
  2. 并行是指,操作同时通知到服务端,服务端可以一起处理(比如双核 CPU,就能开辟两个进程,同时执行两个任务,所以并行是依赖 cpu 的多核来实现),在后端语言中,采用多进程的方式,让系统更加稳定,而且可以实现高并发。

webpack 开启多进程打包优化,其实是根据设备内核数并行打包哦。

Node 中的 this/global

我们一般认为 node 中,this 指向 global,但是 node 在执行的时候为了实现模块化,会在执行代码时,外部包装一个函数,这个函数在执行的时候,为了使模块拥有自己的 this,会改变 this 指向到一个空对象上。

function modluleA() {
  console.log(this);
}

// 实际执行的是下面这行代码 
modluleA.call({}); // {}

如果想用 global 怎么办?

console.log(global); // Object [global] { ... }

如果想把隐藏属性也打出来怎么办?

console.dir(global, { showHidden: true }); 

global 上的一些属性

通过官方文档我们看到,global 上有 Buffer、process、__filename、__dirname、export、module、require这些属性,但是标红的部分,在官方文档都有如下介绍。

This variable may appear to be global but is not.
// 这个变量可能看起来是全局变量,其实并不是。

这些变量不能通过 global. 去访问,但确实随时随地都可以拿到,怎么做的呢?我们上面提到,node 每个文件都被认为是一个模块,会用函数去包裹执行,其实这五个只是函数内部的局部变量而已啦。

function modluleA(__filename, __dirname, export, module, require) {
  console.log('这是模块内部');
}

我们挨个来认识下这几个 api

process『进程』

process.platform查看当前系统

查看当前代码运行的系统,window系统 => win32,mac系统 => darwin

console.log(process.platform); // darwin

process.cwd 查看当前工作目录

是个函数,返回当前的工作目录(执行命令时的路径) cwd 区别于 __filename & __dirname,它们都是绝对路径,不过 __filename & __dirname 是根据当前文件所在的位置决定的,而 cwd 是根据当前运行命令执行的位置决定的,也就是运行时决定的。

// node/global/index.js
console.log(process.cwd()); 
// --------> /Users/ys/Desktop/2021-code


console.log(__filename); 
//  ------->  /Users/ys/Desktop/2021-code/node/global/index.js


console.log(__dirname); 
//  ------->  /Users/ys/Desktop/2021-code/node/global

// 在 /Users/ys/Desktop/2021-code 去执行 node
node ./node/global/index.js

process.env 环境变量对象

运行代码时的环境变量,默认拿到的是当前环境中内设的变量对象,我们想增加自己的环境变量怎么办?

window 下我们通常用 set 命令来设置环境变量,而 mac 下使用的是 export,为了统一两种设置方式,有人写了个 cross-env 的包

// 这里演示 mac 环境下,window 只需要把 export 换成 set 即可
export a=1 && node node/global


console.log(process.env.a); // 1 

需要注意的是,这样添加的变量并不会保存到我们的本地,命令行窗口关掉,添加的环境变量就没啦。

process.argv 执行命令时所带的参数

// 它默认的两个参数 一般我们用不到
//   @1 代表的是 node 文件的路径
//   @2 执行的是哪个文件
console.log(process.argv);

 // 我们通常会截取后面的参数
console.log(process.argv.slice(2));

> node node/global/index.js abc=1  // 输出 [ 'abc=1' ]

不过我们更常用的并不是使用 abc=1 这样来传值,而是通过 --port 3000 这样,如何解析呢?

> node node/global/index.js --port 3000 --conf .env

let args = process.argv.slice(2);
// [ '--port', '3000', '--config', '.env' ]

let userArgs = args.reduce((prev, next, idx, arr) => {
  if (next.includes('--')) {
    prev[next.slice(2)] = arr[idx + 1]
  }

  return prev;
}, {});

console.log(userArgs); // { port: '3000', conf: '.env' }

process.nextTick 本轮代码末尾执行

异步微任务,但是不属于事件循环的一部分,相当于当前代码执行完毕,会优先调用它的回调函数。 也就是说,只要本轮任务的同步代码执行完毕,nextTick 就会立即执行。

const fs = require('fs');

fs.readFile('./1.txt', (err, data) => {
  console.log('文件读取完成');

  setTimeout(() => {
    console.log('timeout');
  });
  
  process.nextTick(() => {
    console.log('nextTick run');
  })
});

// 1111
// 文件读取完成
// nextTick run
// timeout

Node 中常用模块

node 中的常用模块分为三种:

  1. 内置模块、核心模块(不需要安装直接能用,node 自带的)
  2. 文件模块(自己实现的模块)
  3. 第三方模块(比如 npm 包)

path (路径处理)

const path = require('path');

path.join 路径拼接

path.join('a', 'b', 'c'); //  =====> a/b/c

path.join(__dirname, 'a'); // =====> /Users/ys/Desktop/node/path/a

path.resolve 路径拼接,绝对路径

// 根据工作目录,做拼接
path.resolve('a', 'b', 'c'); //  =====> /Users/ys/Desktop/node/a/b/c

path.resolve(__dirname, 'a'); // =====> /Users/ys/Desktop/node/path/a

path.basename 路径截取

console.log(path.basename('a.js', 's')); // ======> a.j

path.extname 获取路径扩展名

console.log(path.extname('a.js')); // ======> .js

path.relative 获取两个地址之间的相对路径

console.log(path.relative('a/b/c', 'c')); // =====> ../../../c

vm (虚拟机模块,单独作用域执行字符串)

其实这个用的不多,它可以让一个字符串执行,和 eval 和 new Function() 执行字符串类似,不过这两种方式都存在一定的问题。

const vm = require('vm');

eval('console.log(path.join)'); // [Function: join]
// new Function 声明的函数直接污染全局
global.c = 100;
const sum = new Function('a', 'b', 'return a + b + c');

console.log(sum(1, 2)); // 103

eval 和 new Funtion 内部字符串执行可能会受外部影响,所以 node 中的实现没有采用 eval 和 new Funtion,而是单独提供了个 vm,它可以单独的去产生一个作用域

global.a = 100;

// 沙箱执行,实现一个全新的上下文
vm.runInNewContext('console.log(a)'); // a is not defined

思考:如使用js实现沙箱机制?

待续。。。