简介
node.js是一个运行在chromeJavascript运行环境下(俗称GoogleV8引擎)的开发平台,用来方便快捷的创建服务器端网络应用程序。你可以把它理解为一个轻量级的JSP或PHP环境,但是用来开发Web应用的话,有时要便捷很多。
nodejs 特点
Node.js 作为一门新兴的后台语言平台,旨在帮助程序员快速构建可伸缩的应用程序,自发布以来,广受开发人员的关注。Node.js 之所以这么受欢迎归功于它的一些吸引人的特点。
- 它是一个 JavaScript 的运行环境:Node.js 作为运行环境可以让 JavaScript 脱离浏览器,在服务器端单独执行。如果客户端和服务器端使用相同的开发语言,可以在很大程度上达到客户端和服务器端代码的共用。
- 依赖于 Chrome V8 引擎进行代码的解析:Chrome V8 负责在非浏览器解析情况下解析 JavaScript 代码。
- 事件驱动(Event-Driven):对于事件驱动来说,在学习 JavaScript 的初级阶段,都会接触到事件,如
click
,load
等,这些事件通常都会绑定在某个页面元素上,然后为其指定事件处理函数,当事件被触发时才会执行相应的处理函数。可以说这样的事件处理机制就是标准的事件驱动机制。 - 非阻塞I/O(non-blocking I/O):提到非阻塞 I/O,首先就有必要了解一下阻塞 I/O,I/O(input/output)表述的是输入/输出操作,阻塞 I/O 可以理解为被阻塞了的输入/输出操作,在服务器端有很多会涉及阻塞 I/O 的操作,例如在读取文件的过程中,需要等待文件读取完毕后才能继续执行后面的操作, Node.js 中使用事件回调的方式来解决这种阻塞 I/O 的情况,避免了阻塞 I/O 所需的等待,所以说它具有非阻塞 I/O 的特点。
- 轻量、可伸缩,适用于实时数据交互应用:在 Node.js 中,Socket 可以实现双向通信,例如聊天室就是实时的数据交互应用。
- 单进程、单线程:进程就是一个应用程序的一次执行过程,它是一个动态的概念。而线程是进程中的一部分,进程包含多个线程在运行。单线程就是指进程中只有一个线程,阻塞 I/O 模式下一个线程只能处理一个任务,而在非阻塞 I/O 模式下,一个线程永远在处理任务,这样 CPU 的利用率是 100%。Node.js 采用单线程,利用事件驱动的异步编程模式,实现了非阻塞的 I/O。
优点及缺点
优点:Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,异步编程,轻量、可伸缩,适于实时数据交互应用。
缺点:单进程,单线程,只支持单核cpu,不能充分的利用多核cpu服务器。一旦这个进程崩掉,那么整个web服务就崩掉了。
nodejs环境搭建
要想搭建环境就要先了解什么是npm
npm其实是Node.js的包管理工具(package manager)
为啥我们需要一个包管理工具呢?
因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。
于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。
更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。
学习 npm 其实就是学习里面相关的命令:
查看 npm 版本指令:
npm -v
npm version
初始项目
npm init
npm init -y
安装模块和卸载模块
// 安装模块
npm install <模块名>
npm i <模块名>
// 卸载模块
npm uninstall <模块名>
npm un <模块名>
安装模块需要联网,也需要时间一般几秒到十几秒即可,这个图就是正在下载模块。
全局安装和局部安装的区别?
全局安装是安装到nodejs的安装目录,一般例如脚手架工具、nodemon、typescript 这些,一般都是全局安装,而且那边的bin目录是在你的path中的,你可以很方便在命令行中直接调用那里的工具。
本地安装安装到本地往上找package.json存在的目录的node_modules中。用来构建本地项目(或者支撑nodejs运行,用require可以直接引用)。
查看包目录
npm root -g
镜像安装
由于 npm 的服务器是在国外,就导致经常我们安装包的时候,容易失败。
有两个解决方案(1)安装 cnpm (2)修改npm的镜像
cnpm 是国内淘宝团队推出的一个工具,cnpm 工具拉包的时候,是从国内的服务器来进行包拉取,cnpm 和 npm同步的频率是5分钟就会同步一次。
npm i -g cnpm
在全局安装了 cnpm 之后,后面我们安装模块就可以使用
cnpm i <模块名>
接下来我们来看第二个解决方案。
npm config list //该命令能够查看 npm 的配置。
通过该配置,我们可以看到 npm 默认的镜像指向 npmjs,那么,我们就可以将这个指向修改成 taobao
npm config set registry https://registry.npm.taobao.org
然后使用 npm config gset registry查看镜像指向
cnpm 是国内淘宝团队推出的一个工具,cnpm 工具拉包的时候,是从国内的服务器来进行包拉取,cnpm 和 npm同步的频率是5分钟就会同步一次,所以不用担心添加的模块找不到。
查看安装的模块
有些时候,我们想要知道当前安装了什么模块,使用 npm list 命令,可以使用 --depth 后缀来调节深度
npm list --depth <0|1|2>
深度由自己决定
清除缓存
有些时候,我们安装依赖失败,是因为之前的包有残留,所以这个时候需要清除缓存
npm cache clean --force
有趣的是,在清楚缓存的时候还会提醒你 o^o
yarn
yarn 同样是一个包管理器,是 Facebook,Google,Exponent 和 Tilde 开发的一款新的 JavaScript 包管理工具。
相比 npm,解决了一些npm存在的问题:
- 安装的时候无法保证速度/一致性
- 安全问题,因为 npm 安装时允许运行代码
安装方式如下:
npm i -g yarn
yarn 也同样存在修改镜像的问题,基本和 npm 大同小异:
修改如下:
yarn config set registry https://registry.npm.taobao.org
获取镜像如下:
yarn config get registry
nodejs 最大的两个特点
一个是异步无阻塞,另一个是事件驱动。
传统的同步代码,当线程在执行中遇到磁盘读写、网络通信等 I/O 操作时,通常要耗费较长的时间。这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞。
当 I/O 操作完毕时,操作系统会将这个线程的阻塞状态解除,恢复其对 CPU 的控制权,令其继续执行。这种 I/O 模式就是通常的同步式 I/O(Synchronous I/O)或阻塞式 I/O(Blocking I/O)。如下图:
我们先来看一看阻塞,下面的代码是一个阻塞I/O的示例:
const fs = require('fs');
console.log('开始写入文件');
try{
// try 里面是尝试执行一段代码,如果出现问题,catch 负责捕捉
console.log('正在写入文件');
fs.writeFileSync('./test.txt','这是一个测试');
} catch(e) {
console.log('文件写入失败');
console.log(e.message);
}
console.log('文件写入完毕');
可以看到try catch阻塞了最后一行代码的执行
nodejs 采用的是异步编程。使用异步式 I/O 与事件紧密结合的编程模式,可以很好的解决 I/O 阻塞的问题。
当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。
为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。如下图:
下面的代码演示了非阻塞I/O的形式:
const fs = require('fs');
console.log('开始写入文件');
fs.writeFile('./test2.txt','这是一个测试2',function(err){
if(err) throw err;
console.log('正在写入文件');
})
console.log('文件写入完毕');
可以看到最后一行代码没有被阻塞。
注意:在 nodejs 中,异步回调的特点,错误优先。
如果是同步代码,可以通过 try…catch 的方式来捕捉错误,但是在异步的情况下,我们无法使用 try…catch 的方式来捕捉错误,所以在 nodejs中规定,把错误对象作为回调函数的第一个参数。
if(err) throw err;
事件驱动
nodejs 的另外一个特点,事件驱动,这里有两个知识点:EventEmitter 和 事件驱动模式。
EventEmitter
在浏览器环境下,常见的事件有 click、mouseup、mousedown 等,这些事件是浏览器环境为我们提前定义好了的。在 nodejs 里面,我们就可以自己去定义事件类型,自己去触发这个事件。
下面是一个 eventEmitter 的示例:
const EventEmitter = require('events').EventEmitter;
const event = new EventEmitter();
// 手动定义一个 test 事件
event.on('test',function(){
console.log('test 事件被触发');
});
// emit 方法表示我要触发一个事件,事件的名字叫做 test
setTimeout(function(){
event.emit('test')
},2000)
还可以绑定多个事件:
const EventEmitter = require('events').EventEmitter;
const event = new EventEmitter();
// 手动定义一个 test 事件
event.on('test',function(){
console.log('test 事件被触发');
});
event.on('test2',function(){
console.log('test2 事件被触发');
});
// emit 方法表示我要触发一个事件,事件的名字叫做 test
setTimeout(function(){
event.emit('test'); //事件一
event.emit('test2'); //事件二
},2000)
在实际的开发中,我们一般不会用到 EventEmitter,但是这个 EventEmitter 是其他很多模块的父类,fs,net,http 等这些模块都是 EventEmitter 子类。
事件驱动模式
当代码在执行过程中遇到类似 AJAX 请求、setTimeout 时间延迟、DOM事件的用户交互等操作时,这些任务可能会消耗较长的时间去执行,但其实却并不消耗 CPU。
可由于 JavaScript 单线程的特点,导致后面的代码也必须等待。而这种无意义的等待成了一种空等、资源浪费。因此 JavaScript 语言的设计者最终在 JavaScript 的执行机制中引出了异步执行操作。
于是,程序中所有的任务分成了两种,一种是同步任务,另一种是异步任务。
同步任务(synchronous):在主线程上排队执行的任务,只有前一个同步任务执行完毕,才能执行后一个同步任务;
异步任务(asynchronous):不进入主线程、而进入"任务队列"(task queue)的任务,只有等主线程同步任务执行完毕,"任务队列"开始通知主线程,请求执行异步任务,该任务才会进入主线程执行。
JavaScript 代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
JavaScript 中,任务队列分为两类:
- 宏任务队列(macro tasks):包括 script(整体代码)、setTimeout、setInterval、I/O、UI rendering 等。
- 微任务队列(micro tasks):包括 Promise、MutationObserver(HTML5 新特性) 等。
来自不同任务源的任务会进入不同的任务队列,宏任务队列可以有多个,微任务队列只有一个。
事件循环的顺序,决定了 JavaScript 代码的执行顺序。
- 取出一个宏任务执行。执行完成后,进入下一步。
- 取出一个微任务执行。执行完成后,继续取出下一个微任务执行。直到微任务队列为空,进入下一步。
- 更新 UI 渲染。
- 也就是说,事件循环的第一次循环从宏任务 script(整体代码)开始。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再次从宏任务开始,当其中一个宏任务队列执行完毕,然后再执行所有的微任务,这样一直循环下去。
其中,第3步(更新 UI 渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新 UI 成本大,所以,一般都会比较长的时间间隔,执行一次更新。
nodejs 系统架构
我们整个 nodejs,是由 V8 + 由C/C++ 的一些库一起封装而成的。不是说整个 nodejs 是单线程,只能说nodejs 中负责处理 js 代码的那个部分是单线程,nodejs 中处理异步模块采用的 C/C++ 那些库,使用的是多线程的方式来执行的代码。