NodeJs学习笔记1

  |  

前言

不会Node.js的前端和咸鱼有什么区别,开始Node.js学习之路


19



开始使用Node.js编程

跳过那些无聊的介绍,直接进入最有意思的编程部分

Node.js命令行工具

当我们将Node.js的安装路径放入到系统的环境变量时,我们就可以在CMD中使用node命令。

运行Node.js程序的基本方法就是执行node script.js,其中script.js是脚本文件名。

此外我们还可以使用node -e "console.log('Hello World')"的方式执行语句,可以把要执行的语句作为node -e的参数

使用node的REPL模式:

REPL(Read-eval-print loop),即输入——求值——输出循环。运行无参数的node将会启动一个JavaScript的交互式shell。

进入REPL模式以后,会出现一个”>”提示符提示你输入命令,输入后按回车,Node.js将会解析并执行命令。如果你执行了一个函数,那么REPL还会在下面显示这个函数的返回值。如果你输入一个错误的指令,REPL则会立即显示错误并输出调试栈。

在任何时候,连续按两次的Ctrl+C即可退出Node.js的REPL模式。

建立HTTP服务器

Node.js与ASP、PHP最大的不同在于,Node.js不需要配置HTTP服务器(譬如Apache、IIS或Nginx)。

Node.js将“HTTP服务器”这一层抽离,直接面向浏览器用户。

我们先来实现一个简单的HTTP服务器:

1
2
3
4
5
6
7
8
var http = require('http');

http.createServer(function(req, res){
res.writeHead(200, {"Content-type": "text/html"});
res.write("<h1>Node.js</h1>");
res.end("<p>Hello World");
}).listen(3000);
console.log("HTTP server is listening at port 3000.");

这样我们就在3000端口实现了一个HTTP服务器

使用supervisor:

在开发Node.js实现HTTP应用时会发现,无论你修改代码的哪一部分,都必须终止Node.js再重新运行才会奏效。这是因为Node.js只有在第一次引用到某部分时才会去解析脚本文件,以后都会直接访问内存,避免重复载入。Node.js的这种设计虽然有利于提高性能,但是却不利于开发调试。因为我们在开发过程中总是希望修改后立即看到效果,而不是每次都要终止进程并重启。

supervisor可以帮助你实现这个功能,它会监视你对代码的改动,并自动重启Node.js。

使用方法很简单,首先使用npm安装supervisor:

1
npm install -g supervisor

接下来使用supervisor命令启动脚本解决就行了

1
supervisor app.js

异步式I/O与事件式编程

Node.js最大的特点就是异步式I/O(或者非阻塞I/O)与事件紧密结合的编程模式。这种模式与传统的同步式I/O线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。

想要了解异步式,先得知道阻塞与线程的概念

阻塞与线程:

线程在执行汇总如果遇到磁盘读写或网络通信(统称为I/O操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的CPU控制权,使其暂停执行,同时将资源让给其它的工作线程,这种线程调度方式叫阻塞。当I/O操作执行完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种I/O模式就是通常的同步式I/O(Synchronous I/O)或阻塞式I/O(Blocking I/O)

异步式 I/O (Asynchronous I/O)或非阻塞式 I/O (Non-blocking I/O)则针对 所有 I/O 操作不采用阻塞的策略。当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作 的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作 系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个 事件。为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予 以处理。

阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞 模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%, I/O 以事件的方式通知。在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞 时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。而在非阻塞模式 下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况 下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么 Node.js 使用了单 线程、非阻塞的事件编程模式。

同步式I/O和异步式I/O的特点

同步式I/0(阻塞式) 异步式I/O(非阻塞式)
利用多线程提供吞吐量 单线程即可实现高吞吐量
通过事件片分割和线程调度利用多核CPU 通过功能划分利用多核CPU
需要由操作系统调度多线程使用多核CPU 可以将单进程绑定到单核CPU
难以充分利用CPU资源 可以充分利用CPU资源
内存轨迹大、数据局部性弱 内存轨迹小,数据局部性强
符合线性的编程思维 不符合传统编程思维

回调函数:

让我们来看看Node.js如何异步读取一个文件:

1
2
3
4
5
6
7
8
9
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data){
if(err){
console.error(err);
} else {
console.log(data);
}
});
console.log("end.");

运行结果是:

1
2
end.
Contents of the file.

同时Node.js也提供同步读取文件的API:

1
2
3
4
var fs = require('fs');
var data = fs.readFileSync('file.txt', 'utf-8');
console.log(data);
console.log("end.")

运行结果是:

1
2
Contents of the file.
end.

这就是同步式和异步式的区别。fs.readFile接收了3个参数,第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。

fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的 事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end.,再看到 file.txt 文件的内容。

事件

Node.js所有的异步I/O操作在完成时都会发送要给事件到事件队列。在开发者看来,事件由EventEmitter对象提供。下面我们用一个简单的例子说明EventEmitter的用法:

1
2
3
4
5
6
7
8
9
10
var EventEmitter = require('events).EventEmitter;
var event = new EventEmitter();

event.on('some_event', function() {
console.log('some_event occured.');
});

setTimeout(function() {
event.emit('some_event');
}, 1000);

event对象注册了时间some_event的一个监听器,然后通过setTimeout在1000毫秒以后向event对象发送事件some_event,此时会调用some_event的监听器

Node.js的时间循环机制:

Node.js 程序由事件循环开始,到事件循 环结束,所有的逻辑都是事件的回调函数,所以 Node.js 始终在事件循环中,程序入口就是 事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出 I/O 请求或 直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未 处理的事件,直到程序结束

模块和包

模块(Module)和包(Package)是 Node.js 重要的支柱。Node.js 的模块和包机制的实现参照了 CommonJS 的标准,但并未完全遵循。

我们经常把 Node.js 的模块和包相提并论,因为模块和包是没有本质区别的,两个概念 也时常混用。如果要辨析,那么可以把包理解成是实现了某个功能模块的集合,用于发布和维护。

什么是模块

模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。
在前面章节的例子中,我们曾经用到了 var http = require(‘http’),其中 http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过 require 函数获取了这个模块,然后才能使用其中的对象。

创建及加载模块

在Node.js中,创建一个模块非常简单,因为一个文件就是一个模块,问题在于如何获取。Node.js提供了exports和require两个对象,其中exports是模块的公开接口,require用于从外部获取一个模块的接口,即所获取模块的exports对象。

我们先创建一个模块文件module.js

1
2
3
4
5
6
7
8
9
var name;

exports.setName = function(theName) {
name = theName;
};

exports.sayHello = function() {
console.log('Hello ' + name);
};

然后在同目录创建引用文件getmodule.js

1
2
3
var myModule = require('./module');
myModule.setName('Double--R');
myModule.sayHello();

单次加载:

require不会重复加载模块,也就是说无论调用多少次require,获得的模块都是同一个

1
2
3
4
5
var hello1 = require('./module');
hello1.setName('test1');
var hello2 = require('./module');
hello2.setName('test2');
hello1.sayHello();

最终的输出结果是hello2,这是因为变量hello1和hello2指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,终输出结果是 由后者决定的。

覆盖exports:

有时候我们只是想把一个对象封装到模块中,例如:

1
2
3
4
5
6
7
8
9
10
11
function Hello() {
var name;
this.setName = function(theName) {
name = theName;
};
this.sayHello = function() {
console.log("Hello " + name);
};
}

exports.Hello = Hello;

此时我们在其他文件中需要通过 require(‘./singleobject’).Hello 来获取 Hello 对象,这略显冗余,可以用下面方法稍微简化:

1
2
3
4
5
6
7
8
9
10
11
function Hello() {
var name;
this.setName = function(theName) {
name = theName;
};
this.sayHello = function() {
console.log("Hello " + name);
};
}

module.exports = Hello;

这样就可以直接获得这个对象了:

1
2
3
4
5
var Hello = require('./singleobject'); 

hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello= Hello。在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。 事实上,exports 本身仅仅是一个普通的空对象,即 {},它专门用来声明接口,本 质上是通过它为模块闭包的内部建立了一个有限的访问接口。因为它没有任何特殊的地方, 所以可以用其他东西来代替,譬如我们上面例子中的 Hello 对象。

文章目录
  1. 1. 开始使用Node.js编程
    1. 1.1. Node.js命令行工具
    2. 1.2. 建立HTTP服务器
  2. 2. 异步式I/O与事件式编程
    1. 2.1. 阻塞与线程:
    2. 2.2. 回调函数:
    3. 2.3. 事件
  3. 3. 模块和包
    1. 3.1. 什么是模块
    2. 3.2. 创建及加载模块