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 想让 A 厨师给炒个西红柿鸡蛋,线程2 也想让 A 厨师给排个黄瓜,这时候,就会有锁的概念,A 厨师炒完西红柿鸡蛋,再给线程2 拍黄瓜。
缺陷:多个线程看起来貌似是并行处理,实际上是不停的切换时间片去执行,比如线程1 执行 50%,跳到线程2 执行 50%,再跳到线程3执行 50%,然后回到线程1,这样反复的轮流加载,其本身是同步的,存在阻塞,并且线程池为空时,更是直接阻塞,所以高并发场景下的性能受限。
node 是什么样的
Node中,是单线程非阻塞异步的,也就是说,一旦我有多个请求发送到服务器,服务器使用的是同一个线程来处理,如下图:

- 每个操作使用同一个线程,而且线程不会等待异步操作的结果,而是基于事件回调来处理异步。
- 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 做中间层来解决跨域问题
并发和并行
乍一听,二者好像都一样,其实有着本质的区别。
- 并发是指,操作同时到达到服务端,但是服务端只能依次处理。
- 并行是指,操作同时通知到服务端,服务端可以一起处理(比如双核 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 中的常用模块分为三种:
- 内置模块、核心模块(不需要安装直接能用,node 自带的)
- 文件模块(自己实现的模块)
- 第三方模块(比如 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实现沙箱机制?
待续。。。