关于Node.js,你知道多少?

977 阅读12分钟

引言

  • 定义:

nodeJS是基于chrome V8引擎的javascript运行环境。 nodeJs使用了一个事件驱动非阻塞的异步I/O模型,使其轻量又高效。

  • javascript中在浏览器中运行和在nodeJs中运行的区别 没有浏览器中的一些API(document/window);加了许多的nodeJs的API;

  • 浏览器 event loop

image.png

  • nodeJs可以现阶段可以用来做什么项目,这些项目为什么会采用nodeJs作为他们的底层技术?
nodeJs应用: web服务的开发、构建工作流、web 
-》 场景一:web服务的开发
- 最广泛的应用就是web服务的开发:搜索引擎的优化,网页首屏的加速。即这两个需求决定了我
们要做一个服务端的渲染 
- 服务端渲染 + 前后端同构(代码复用的问题,同一份代码同时在前端渲染也在后端渲染) = nodeJs
-》 场景二: 构建工作流
即打包工具gulp和webpack是用nodeJs来写的。 在没有nodeJs的时候,可能会使用java来做构建工具,但是前端同学很难查找问题以及对构建工具的设计做出贡献。 
- visual Studio Code 也是用nodeJs做的开发者工具 ,底层基于的技术是electron (在nodeJs基础上封装了一层chrome浏览器的内核)这样就可以让开发者在chrome里面再跑一个nodeJs或者在nodeJs中再跑一个chrome
-》大型应用需要给使用者自定义模块的能力:
使用Node.js做复杂本地应用,利用node.js的灵活性提供外部扩展,同时利用js庞大的开发者技术让这种灵活性得到应用。
应用: 游戏直播鼻祖: twitch.tv;   沙盒类生存游戏: wayward

node事件循环和浏览器的区别

zhuanlan.zhihu.com/p/54882306

说起node,它应该是作为前端的我们与后端语言距离忽而最近却又最远的知识了。大前端盛行的时代下各种技术纷至沓来,很多同学早已应接不暇,我们应该如何迈向下一步呢,因此接下来我们来探讨一下为什么要学习node呢?

  • 提高技术竞争力,提升在团队存在感
  • 很多项目是需要做中间层的,而node就是最好且唯一的选择
  • 我们不可能一辈子只做前端或者只做后端,随着时间的拉长后起之秀会有很多,然对于前端同学来说,node无非是最好的对后端切入窗口。 本篇文章小编仅简单介绍node基础

思维图

我画了一个图,帮助大家理解 本篇仅介绍图中模块

正文

node.js的三层

  1. Node.js标准库,用js编写即使用过程中可以直接调用的api,在源码的lib文件可以看到
  2. Node bindings,这一层是js与底层C/C++能够沟通的关键,js可以通过它调用C++进行交换数据
  3. 这一层是支撑Node.js运行的关键,由C/C++实现
V8: 为 Javascript 提供了在非浏览器端运行的环境
Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。
C-ares:提供了异步处理 DNS 相关的能力。
http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

image.png

那什么是libuv?

几乎所有和操作系统打交道的部分都离不开 libuv的支持。libuv也是node实现跨操作系统的核心

image.png

为什么js有了基于V8引擎的node就可以与操作系统进行如此底层的交互(即跨系统)

image.png

因为: Node核心模块(通过node-bindings)调用C++内建模块,C++内建模块通过libuv进行系统调用。

也就是说其实是C++和libuv实现的。

javascript是单线程的,并且是异步的。浏览器中有其他线程帮助js实现异步。那node.js如何实现异步的呢?

js执行线程是单线程,把需要的i/o交给libuv,libuv在指定的时刻回调就好了。

细化一下: 怎么把i/o交给libuv的呢?

1、发起 I/O 调用
用户通过 Javascript 代码调用 Node 核心模块,将参数和回调函数传入到核心模块;
Node 核心模块会将传入的参数和回调函数封装成一个请求对象;
将这个请求对象推入到 I/O 线程池等待执行;
Javascript 发起的异步调用结束,Javascript 线程继续执行后续操作

2. 执行回调
I/O 操作完成后,会取出之前封装在请求对象中的回调函数,执行这个回调函数,以完成 Javascript 回调的目的

libuv线程池来模拟异步回调:

image.png

node.js是通过libuv和事件循环来实现的,这就是为什么js是单线程语言,能在node.js中实现异步的原因。

node.js单线程、异步调用、非阻塞/IO,那怎么解决高并发呢?

node.js的单线程指的是js运行环境的单线程,有了libuv的线程池,nodejs并不是单线程的,而且还有并行事件发生。线程池默认大小是4,即同时有4个线程去做文件i/o的工作,剩下的请求会被挂起直到线程池有空闲。所以说nodejs对于并发数是有限制的。

设置方法:

通过环境变量设置: UV_THREADPOOL_SIZE 
代码中设置: process.env.UV_THREADPOOL_SIZE

node应用场景

Node.js 通过 libuv 来处理与操作系统的交互,并且因此具备了异步、非阻塞、事件驱动的能力。因此,NodeJS能响应大量的并发请求。所以,NodeJS适合运用在高并发、I/O密集、少量业务逻辑的场景。不适合CPU密集型任务场景。

如果操作系统本身就是单核,那也就算了,但现在大部分服务器都是多 CPU 或多核的,而 Node.js 只有一个 EventLoop,也就是只占用一个 CPU 内核,当 Node.js 被CPU 密集型任务占用,导致其他任务被阻塞时,却还有 CPU 内核处于闲置状态,造成资源浪费。

node核心模块

在开始之前先简单介绍一下npx,现在我们装的node都会带上npm,同时会有npx(从npm5.2开始), npx功能:

  • 调用项目安装的模块 之前 node-modules/.bin/mocha --version

现在 npx mocha --version

  • 避免全局安装模块 之前:npm install -g create-react-app create-react-app my-project

现在:npx create-react-app my-project

因为npx将create-react-app现在到一个临时目录,使用之后再删除

  • --no-install参数和--ignore-existing参数 强制使用本地模块: npx --no-install http-server

强制使用远程模块: npx --ignore-existing create-react-app my-react-app


下面小编就要放开心扉啦哈哈,各位小明同学们也要畅所欲言喽,首先我们来看下全局变量的概念。

node全局变量

小编:全局变量也就是可以我们可以在全局上访问到的属性,而可以在全局上可以访问到的属性不一定是全局变量!

小明:那个不一定指的是?

小编:同学你知道commonJs规范么,__dirname,__filename,export,module,require就不是全局变量,但是他们是可以在全局中访问到的。如果你想知道原理,可以在评论区留言并关注,因为接下来我会写一篇commonJS原理源码剖析。

那global全局变量有什么呢?

console.log('global',global)  ;//并run code 

global Object [global] {
  global: [Circular],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function]
  }
}
//如果你想查看所有的全局变量 
console.log(global,{showHidden:true})
//这里省略300行。。。实在是太长了,了解几个常用的就可以了 please following me。

process进程

所有的前后端代码都会跑在进程上。是不是觉得process这么熟悉,和webpack中的process是一个吗?是的,webpack就是node写的嘛,当然是同一个process。

列几个常用的process属性

[
  'version',          		          //版本
  'platform',        		         //代码运行在什么平台下
  'memoryUsage',    		        //内存占用情况
  'nextTick',      		       //是一个函数
  'argv',        		      //参数:用户执行时命令行传递的参数
  'chdir',       		     //变更nodejs当前工作目录
  'cwd',        		    //current working directory 当前工作目录(可以改变)
  'env',                           //默认读取全局环境变量
]
process.cwd():输出命令运行的工作目录,即cd..切换到learn-node上级目录Desktop之后,node learn-node/1.js,输出......./Desktop。因此不常用,一般用绝对路径 __dirname。
process.chdir():变更nodeJs当前工作目录,既然是变更工作目录,那肯定是结合process.cwd()来使用的,
try{
        console.log(process.cwd());            //    /Users/yourName/Desktop/learn-node
        process.chdir("../../../yourName")     //
        console.log(process.cwd());            //    /Users/yourName
}catch(err) {
	console.log(err)
}
要是不想这么麻烦,就直接绝对路径吧!
process.env:默认读取全局变量 (可以在终端设置临时变量,只针对当前环境,关闭终端窗口就释放了)
设置环境变量并运行:cross-env a1=development node 1.js

process.argv:命令行运行时传递的参数。
//比如: node 1.js   --port 3000 --config webpack.config.js
//console.log(process.argv)
[
  '/usr/local/bin/node',                           //node的可执行文件
  '/Users/yourName/Desktop/learn-node/1.js',       //node命令执行的哪个文件
  '--port',                                        //...other  就是用户传递的参数,进行参数解析
  '3000',
  '--config',
  'webpack.config.js'
]
  • process.argv是一个数组,我们需要对数组进行解析,找出我们需要的内容
let program = {};
process.argv.slice(2).forEach( (item,index,array) => {
	if(item.startsWith('--')) {
		program[item.slice(2)] = array[index + 1]
	}
})
console.log(program); //这就是我们解析之后的对象
//node 1.js --port 3000 --config webpack.config.js
//program { port: '3000', config: 'webpack.config.js' }

因此有了第三方包commander帮我们做解析参数的工作: 我的另一篇文章》 www.yuque.com/linhao-00ft…

  • process.memoryUsage():内存占用情况
 {
  rss: 19693568,
  heapTotal: 4907008,
  heapUsed: 2870368,
  external: 933161,
  arrayBuffers: 17695
}

node.js事件驱动是如何实现的,即node事件轮询

node中的事件轮询是处理非阻塞I/O操作的机制,node主要写了个libuv库,这个库是用多线程来模拟异步。那他是如何来调度的呢?他和js一样:有线程、可管理同步代码、有事件循环机制。

浏览器和node事件循环机制的区别: js虽然是单线程的来处理的,但是有时会把操作转移到内核中;浏览器和node事件循环在node 10版>本执行结果一样,但是本质不同;浏览器只有两个任务队列,即微任务队列和宏任务队列,但是node有>多个宏任务队列。

参考链接: nodejs.org/zh-cn/docs/…

在每次运行的事件循环之间,node.js会检查自己是否在等待任何异步I/O或计时器,如果没有则完全关闭。图中的timer、poll 、pending callback、check......每一行都是一个队列。所以他的任务是被分配到不同的队列中的,当主栈代码执行完后会进入图中的执行队列中,依次扫描。

timer                  // 定时器:执行已经被setTimeout()、setInterval()调度的回调函数
pending callbacks     // 等待:执行延迟到下一个循环迭代的i/o回调
idle prepare	     //仅限系统内部使用
poll                //几乎所有的异步API的回调:检查新的I/O事件、执行与I/O相关的回调
		   //除了关闭的回调函数,已经被定时器调度的setTimeout/setInterval,或者在适当的时候(回调函数太多了)进行阻塞的函数
check 		  //  setImmediate()函数在这里执行
close  callbacks // 一些关闭的回调函数,如:socket.on('close', ...)

帮助大家理解,给大家画了流程图

看了图有同学可能疑问,为什么要在poll处进行阻塞呢?很简单,如果不阻塞的话就成了死循环啦! 每执行宏任务队列中的一个callback,就会清空一次微任务队列(和浏览器的事件循环机制相同)。上面6个队列[fn,fn,fn]中,timer(定时器)、poll(几乎所有的回调)、check(setImmediate)是宏任务队列,

提到微任务和宏任务就不得不说process.nextTick(),他是在栈底被调用的,即close callbacks后,可以看做是微任务,但优先级process.nextTick() > 微任务

setTimeout(() => {
	console.log('setTimeout')
},0)
promise.resolve().then(res => {
	console.log('then')
})
process.nextTick(() => {
	console.log('nextTick')
})
console.log('ok')
//run code 结果依次为: ok nextTick  then  setTimeout

那setTimeout和setImmediate谁更快呢?

情况1setTimeout(() =>{},0) 这个零,要看性能的影响,要看循环的时候,setTimeout是否已经放到了任务队列,是优先于setImmediate还是在setImmediate之后 // 要看性能,比如我们的代码执行的非常快,定时器还没有来得及放到timer, 那就直接走到了check;也有可能主执行栈执行完了,定时器已经放到了timer中了,那再去走到check中

//正常思维是setTimeout快于setImmediate。但是受性能影响
setImmediate(() => {
    console.log('setImmediate')
})
console.time('start')
setTimeout(() => {
    console.log('setTimeout')
    console.timeEnd('start')
},0)

 
情况2:在pool中: fs.readFile是异步i/o ,也就是poll,>然后check(也就是setImmediate),>然后timer(也就是setTimeout),所以 setImmediate timeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout')
    },0)
    setImmediate(() => {
        console.log('setImmediate')
    })
})
// setImmediate   timeout

模块化

因为模块化内容比较多,所以准备写一篇《模块化及commonJs规范实现原理》,可以点个关注,下周会把链接贴出来!

推荐文章: juejin.cn/post/691450…

内置模块(核心模块)

核心模块是node中内置的模块(区分于自定义模块和第三方模块)

  • events 任务:

要知道什么是发布订阅 2.学会怎么使用events 3. 模仿源码自己实现events功能 回顾发布订阅要戳这里:

别着急,刚写了1/3,未完待续。。。。。。

javascript中只是node应用的一部分,如果项目过大会造成堆栈溢出。因为在node中通过javascript使用内存只能只用部分内存,如果项目过大,webpack编译会占用很多的系统资源,如果超出了V8对node默认的内存限制大小,就是内存溢出。 解决: 调整V8对node默认的内存限制大小,npm run dev --max_old_space_size = 7988