8.nodejs

nodejs

nodejs 简介

nodejs 是 2009 年诞生的,是一个js的运行环境。它可以让我们的js脱离浏览器环境,在一个称之为node的环境里面运行。

有了nodejs之后,我们可以像PHP、Java、Ruby一样做服务器端开发。

nodejs 历史

众所周知,在 Netscape 设计出 JavaScript 后的短短几个月,JavaScript 事实上已经是前端开发的唯一标准。后来,微软通过 IE 击败了 Netscape 后一统桌面,结果几年时间,浏览器毫无进步。(2001 年推出的古老的 IE 6 到今天仍然有人在使用!)

没有竞争就没有发展。微软认为 IE 6 浏览器已经非常完善,几乎没有可改进之处,然后解散了 IE 6 开发团队!而 Google 却认为支持现代 Web 应用的新一代浏览器才刚刚起步,尤其是浏览器负责运行 JavaScript 的引擎性能还可提升 10 倍。

先是 Mozilla 借助已壮烈牺牲的 Netscape 遗产在 2002 年推出了 Firefox 浏览器,紧接着 Apple 于 2003 年在开源的 KHTML 浏览器的基础上推出了 WebKit 内核的 Safari 浏览器,不过仅限于 Mac 平台。随后,Google 也开始创建自家的浏览器。他们也看中了 WebKit 内核,于是基于 WebKit 内核推出了 Chrome 浏览器。

Chrome 浏览器是跨 Windows 和 Mac 平台的,并且,Google 认为要运行现代 Web 应用,浏览器必须有一个性能非常强劲的 JavaScript 引擎,于是 Google 自己开发了一个高性能 JavaScript 引擎,名字叫 V8,以 BSD 许可证开源。

现代浏览器大战让微软的 IE 浏览器远远地落后了,因为他们解散了最有经验、战斗力最强的浏览器团队!回过头再追赶却发现,支持 HTML5 的 WebKit 已经成为手机端的标准了,IE 浏览器从此与主流移动端设备绝缘。

nodejs 诞生

Ryan Dahl,被称之为 Node.js 之父。他原本的工作是用 C/C++ 写高性能 Web 服务。对于高性能,异步 IO 和事件驱动是基本原则,但是用 C/C++ 写就太痛苦了。于是 Ryan Dahl 开始设想用高级语言开发 Web服务。

他评估了很多种高级语言,发现很多语言虽然同时提供了同步 IO 和异步 IO,但是开发人员一旦用了同步 IO,他们就再也懒得写异步 IO 了,所以,最终,Ryan 瞄向了 JavaScript 。

因为 JavaScript 是单线程执行,根本不能进行同步 IO 操作,所以,JavaScript 的这一“缺陷”导致了它只能使用异步 IO。

选定了开发语言,还要有运行时引擎。这位仁兄曾考虑过自己写一个,不过明智地放弃了,因为 V8 就是开源的 JavaScript 引擎。让 Google 投资去优化 V8,咱只负责改造一下拿来用,还不用付钱,这个买卖很划算。

于是在 2009 年,Ryan 正式推出了基于 JavaScript 语言和 V8 引擎的开源 Web 服务器项目,命名为 Node.js。虽然名字很土,但是,Node.js 第一次把 JavaScript 带入到后端服务器开发,加上世界上已经有无数 JavaScript 开发人员,所以 Node.js 一下子就火了起来。

在 Node.js 上运行的 JavaScript 相比其他后端开发语言有何优势?

最大的优势是借助 JavaScript 天生的事件驱动机制加 V8 高性能引擎,使编写高性能 Web 服务轻而易举。

其次,JavaScript 语言本身可以算是半个函数式语言,在前端开发时,开发人员往往写得比较随意,让人感觉 JavaScript 就是个“玩具语言”。但是,在 Node.js 环境下,通过模块化的 JavaScript 代码,加上函数式编程,并且无需考虑浏览器兼容性问题,直接使用最新的 ECMAScript 标准,可以完全满足工程上的需求。

nodejs 特点

Node.js 作为一门新兴的后台语言平台,旨在帮助程序员快速构建可伸缩的应用程序,自发布以来,广受开发人员的关注。Node.js 之所以这么受欢迎归功于它的一些吸引人的特点。

  • 它是一个 JavaScript 的运行环境:Node.js 作为运行环境可以让 JavaScript 脱离浏览器,在服务器端单独执行。如果客户端和服务器端使用相同的开发语言,可以在很大程度上达到客户端和服务器端代码的共用。
  • 依赖于 Chrome V8 引擎进行代码的解析:Chrome V8 负责在非浏览器解析情况下解析 JavaScript 代码。
  • 事件驱动(Event-Driven):对于事件驱动来说,在学习 JavaScript 的初级阶段,都会接触到事件,如clickload等,这些事件通常都会绑定在某个页面元素上,然后为其指定事件处理函数,当事件被触发时才会执行相应的处理函数。可以说这样的事件处理机制就是标准的事件驱动机制。
  • 非阻塞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。
书写第一个nodejs程序

REPL环境,英文全称read-eval-print-loop,翻译成中文就是“即时运行环境”,它的作用就是可以及时的运行 js 代码。

REPL环境主要用于测试一些小片段代码的时候,非常方便。

退出REPL环境,ctrl+c按两次。

模块化

模块化的好处?

因为在计算机程序开发中,随着代码越写越多,一个文件就会越来越长,而且我们也需要分工合作,所以,这个时候就体现出了模块化的重要性。

其实函数(方法)就是模块化思想的一种体现。

总结模块化的好处:

  • 生产效率高
  • 维护成本低
nodejs 中的模块规则

在nodejs中,模块整体可以分为3类:文件模块核心模块第三方模块,引入模块统一使用require方法。

  1. 文件模块

    require(‘路径.扩展名’)

require('./index.js'); // 引入当前目录的 index.js
require('../index.js'); // 引入上一级目录的 index.js
require('/index.js'); // 代表根目录,比如当前的文件在 c 盘,那么就是 c:\index.js
  1. 核心模块

所谓核心模块,就是nodejs里面自带的模块。

在引入核心模块的时候,不需要添加任何路径,直接引入就可以了。

require('os');
require('fs');
  1. 第三方模块

所谓第三方模块,就是指第三方开发的模块,引入的时候同样不需要添加任何路径。

require('mysql');
require('readline-sync');

在模块的引入上面,由于模块拥有不同的类别,所以有不同的优先等级。

优先顺序,从上往下依次为:

  • 核心模块,http、fs、path

  • 文件模块

    • 相对路径(./ 和 …/)
    • 绝对路径(/ 开头)
  • 第三方模块

Commonjs 规范

Commonjs 其实就是一个规范。js 最早是在浏览器端运行,浏览器端的 js,规范来源于ECMAScript。

js 现在是在一个叫做node的环境里面运行,Commonjs 就是 js 在浏览器环境以外的环境的规范,不同于ES,ES是由ECMA(欧洲计算机制造协会)来定的规范,而Commonjs 则是由社区来推动。

ECMAScript 既然是一个规范,那么实现的预言就有多种,最出名的是javascript,当然还有flash里面用到的actionscript。

Commonjs 也是一个规范,所以能实现这个规范也有多种,node,mongodb,couchdb,这些都是Commonjs的一种实现。

Commonjs 里面模块的导入导出

在 Commonjs里面,通过module.exports来导出一个模块,通过require来导入一个模块。

示例如下:

// index2.js
module.exports.name = "xiejie";
module.exports.sayHello = function(){
    console.log('Hello');
}
const obj = require('./index2.js');
console.log(obj.name); // xiejie
obj.sayHello(); // Hello

为了让开发人员使用起来方便,nodejs还提供了一个 exports的对象,它是指向 module.exports

使用示例如下:

// index.js
const obj = require('./index2');
console.log(obj.name); // xiejie
obj.sayHello(); // hahaha
// index2.js
exports.name = "xiejie";
exports.sayHello = function(){
    console.log("hahaha");
}

module.exports 和 exports 有什么区别?

exports 指向 module.exports,module.exports 是我们最终导出的东西,默认是一个对象,但是我们是可以修改的,例如:

// index2.js
module.exports = 'xiejie'

// index.js
const obj = require('./index2');
console.log(obj); // xiejie

但是exports就不行,因为exports是一个指向,如果赋其他值,那么就切断了module.exports的指向

// index2.js
exports = 'xiejie'

// index.js
const obj = require('./index2');
console.log(obj); // {}
NPM

NPM,英文全称 Node Package Manager,翻译成中文就是“node 包管理器”,在 NPM 上面,就有各种各样的包,例如之前咱们用过的 readline-sync。

NPM 官网:https://www.npmjs.com/

学习 npm 其实就是学习里面相关的命令:

  1. 查看 npm 版本
npm -v
npm version
  1. 初始项目
npm init
npm init -y
  1. 安装模块和卸载模块
// 安装模块
npm install <模块名>
npm i <模块名>
  
// 卸载模块
npm uninstall <模块名>
npm un <模块名>

全局安装局部安装的区别?

在上面的安装中,我们属于局部安装,全局安装会添加 -g 这个参数。(global)

// 全局安装
npm install -g <模块名>
npm i -g <模块名>

全局安装的包,一般是我们要使用到里面的命令,这一类包我们会全局安装。一般例如脚手架工具、nodemon、typescript 这些,一般都是全局安装,因为要使用到里面的命令。

  1. 查看包目录
npm root -g
  1. 镜像安装

由于 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
  1. 查看安装的模块

有些时候,我们想要知道当前安装了什么模块,使用 npm list 命令,可以使用 --depth 后缀来调节深度

npm list --depth <0|1|2>
  1. 清除缓存

有些时候,我们安装依赖失败,是因为之前的包有残留,所以这个时候需要清除缓存

npm cache clean --force
yarn

yarn 同样是一个包管理器,是 Facebook,Google,Exponent 和 Tilde 开发的一款新的 JavaScript 包管理工具。

相比 npm,解决了一些npm存在的问题:

  • 安装的时候无法保证速度/一致性
  • 安全问题,因为 npm 安装时允许运行代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QvBRIA1j-1611579927056)(/Users/Jie/Library/Application Support/typora-user-images/image-20200916134752106.png)]

安装方式如下:

如果要使用官方脚本,那就是:

curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --nightly

当然,你也可以使用最方便的形式:

npm i -g yarn

yarn 也同样存在修改镜像的问题,基本和 npm 大同小异:

// 获取当前 yarn 的镜像
yarn config get registry
# -> https://registry.yarnpkg.com

修改如下:

yarn config set registry https://registry.npm.taobao.org
nodejs 两大特点

nodejs 最大的两个特点,一个是异步无阻塞,另一个是事件驱动

阻塞

传统的同步代码,当线程在执行中遇到磁盘读写、网络通信等 I/O 操作时,通常要耗费较长的时间。这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞。

当 I/O 操作完毕时,操作系统会将这个线程的阻塞状态解除,恢复其对 CPU 的控制权,令其继续执行。这种 I/O 模式就是通常的同步式 I/O(Synchronous I/O)或阻塞式 I/O(Blocking I/O)。如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DlZtLGB5-1611579927056)(/Users/Jie/Library/Application Support/typora-user-images/image-20200916142257013.png)]

下面的代码是一个阻塞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('文件写入完毕');

非阻塞:

nodejs 采用的是异步编程。使用异步式 I/O 与事件紧密结合的编程模式,可以很好的解决 I/O 阻塞的问题。

当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。

**为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。**如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9w4vWGrm-1611579927058)(/Users/Jie/Library/Application Support/typora-user-images/image-20200916143201233.png)]

下面的代码演示了非阻塞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中规定,把错误对象作为回调函数的第一个参数。

事件驱动

nodejs 的另外一个特点,事件驱动,这里就两个知识点:EventEmitter,第二个就是事件驱动模式。

1. 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 新特性) 等。

来自不同任务源的任务会进入不同的任务队列,宏任务队列可以有多个,微任务队列只有一个。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRZfXFDl-1611579927059)(/Users/Jie/Library/Application Support/typora-user-images/image-20200916151414699.png)]

事件循环的顺序,决定了 JavaScript 代码的执行顺序。

  • 取出一个宏任务执行。执行完成后,进入下一步。
  • 取出一个微任务执行。执行完成后,继续取出下一个微任务执行。直到微任务队列为空,进入下一步。
  • 更新 UI 渲染。

也就是说,事件循环的第一次循环从宏任务 script(整体代码)开始。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再次从宏任务开始,当其中一个宏任务队列执行完毕,然后再执行所有的微任务,这样一直循环下去。

其中,第3步(更新 UI 渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新 UI 成本大,所以,一般都会比较长的时间间隔,执行一次更新。

nodejs 系统架构

我们整个 nodejs,是由 V8 + 由C/C++ 的一些库一起封装而成的。不是说整个 nodejs 是单线程,只能说nodejs 中负责处理 js 代码的那个部分是单线程,nodejs 中处理异步模块采用的 C/C++ 那些库,使用的是多线程的方式来执行的代码。

etTimeout、setInterval、I/O、UI rendering 等。

  • 微任务队列(micro tasks):包括 Promise、MutationObserver(HTML5 新特性) 等。

来自不同任务源的任务会进入不同的任务队列,宏任务队列可以有多个,微任务队列只有一个。

[外链图片转存中…(img-hRZfXFDl-1611579927059)]

事件循环的顺序,决定了 JavaScript 代码的执行顺序。

  • 取出一个宏任务执行。执行完成后,进入下一步。
  • 取出一个微任务执行。执行完成后,继续取出下一个微任务执行。直到微任务队列为空,进入下一步。
  • 更新 UI 渲染。

也就是说,事件循环的第一次循环从宏任务 script(整体代码)开始。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的微任务。当所有可执行的微任务执行完毕之后。循环再次从宏任务开始,当其中一个宏任务队列执行完毕,然后再执行所有的微任务,这样一直循环下去。

其中,第3步(更新 UI 渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新 UI 成本大,所以,一般都会比较长的时间间隔,执行一次更新。

nodejs 系统架构

我们整个 nodejs,是由 V8 + 由C/C++ 的一些库一起封装而成的。不是说整个 nodejs 是单线程,只能说nodejs 中负责处理 js 代码的那个部分是单线程,nodejs 中处理异步模块采用的 C/C++ 那些库,使用的是多线程的方式来执行的代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值