node.js高级编程阅读笔记

1,934 阅读10分钟
  1. 事件驱动编程是指程序的执行流程取决于事件,当感兴趣的事情发生时由系统调用的函数来取代应用返回值的编程风格被称为事件驱动编程或者异步编程,它是Node的显著特征之一,这种编程风格意味着当前进程在处理I/O操作时不会发生阻塞,因此多个I/O操作可以并行进行,当每个操作结束时,将会分别调用其对应的回调函数,线程是一种轻量级的进程。
  2. 事件驱动编程风格和事件循环相伴相生,事件循环是一个处于不间断循环的结构,该结构主要有两种功能,事件检测和事件触发处理。事件循环只是在一个进程中运行的单个线程,这意味着当事件发生时,可以不用中断就运行事件处理程序。有以下两个特点:
  • 在任意时刻,最多运行一个事件处理程序。
  • 事件处理程序可以不间断地运行直到结束。
  1. js的第一类函数(first-class func),函数可以赋值给变量。
(function(){
    var clickCount = 0;
    $('#mybutton').click(function(){
        clickCount ++;
        alert("a");
    })
}())

闭包可以避免污染全局作用域

  1. 在js中,函数操作并不是孤立地工作而是会记住它被声明时所在的上下文,这能让函数操作其声明时所在的上下文以及父上下文中的所有变量。即使声明回调函数的那个作用域已经返回,回调函数依然可以操作该作用域和父作用域中的任意变量。
  2. js是一门强大的语言,因为它具有第一类函数和闭包,所以很适合事件驱动编程风格。

加载模块

var module = require('module_name');

上面的代码会导入一个核心模块或者由npm安装的模块,require函数会返回一个对象,该对象表示模块对外暴露Js API。根据模块的不同,该对象可能是任意JavaScript值--可以是一个函数,也可以是一个具有若干属性的对象,属性可能是函数、数组或其他任何类型的javascript对象。

  1. 在Node中,文件和模块是一一对应的
    function r_squared() {
        return Math.pow(r,2);
    }
    function area() {
        Math.PI * r_squared()
    }
    return {
        area: area
    }
}

module是一个变量,它表示当前模块自身,而module.exports表示模块向需要它的脚本所导出的对象,它可以是任意对象。

加载核心模块

node中有一些以二进制形式发布的模块,这些模块被称为核心模块,核心模块只能通过模块名引用,不能通过文件路径引用,即使已经存在 一个与其同名的第三方模块,也会优先加载核心模块。 var http = require('http'); 2. 加载文件模块可以用绝对路径和相对路径,还可以使用文件夹路径来加载模块 var myModule = require('./myModuleDir'); 如此一来,Node就会在指定的文件夹下查找模块,Node会假定该文件夹是一个包,并试图查找包定义,包定义包含在名为package.json的文件中。如果文件夹中不存在包定义文件package.json,那么包的入口点会假定为默认值index.js。反之,如果存在package.json文件, 那么Node就会尝试解析该文件并查找main属性,将main属性当作入口点的相对路径。

从node_modules夹加载

如果一个模块名既不是相对路径,也不是核心模块,那么Node就会尝试在当前目录下的node_modules文件夹中查找该模块。

缓存模块

模块在首次加载时会被缓存起来,这意味着如果模块名能被解析为相同的文件名,那么每次调用require('myModule')都会确切的返回 同一模块。

总结

Node取消了js默认的全局名称空间,而用CommonJS模块系统取而代之,这样可以让你更好地组织代码,也因此避免一些安全性问题和错误 。可以使用require()函数从文件或者文件夹加载核心模块、第三方模块或者自定义模块。

命名空间

命名空间(namespace)有的语言称package,是一个在静态语言中常见的概念,js并不支持原生的命名空间,可以创建对象字面量来模拟命名空间。

应用缓冲区处理、编码和解码二进制数据

js善于出来字符串,但由于它最初是被设计用来出来HTML文档的,它并不擅长处理二进制数据。为了使这类二进制数据处理任务变得容易些 Node引入了一个二进制缓冲区实现,该实现以Buffer伪类中的js api形式暴露给外界。

创建缓冲区

var buf = new Buffer('hello world'); // 默认为utf-8
var buf = new Buffer('8b76de713ce', 'base64');
var buf = new Buffer(1024)

在缓冲区中获取和设置数据

console.log(buf[10]);

当创建一个已经被初始化的缓冲区是,该缓冲区中包含的数据并非是0,而是一些随机值。

var buf = new Buffer(1024);
console.log("buf[100]") // -> 5(某个随机值)

切分缓冲区

一旦创建或获取一个缓冲区,可以通过指定开始位置和介绍位置来切分缓冲区,从而创建一个更小的缓冲区

var buffer = new Buffer("this is the content of my buffer");
var smallerBuffer = buffer.slice(8,19);
console.log(smallerBuffer.toString()); // -> "the content"

复制缓冲区

可以使用copy方法将缓冲区中的一部分复制到另一个缓冲区中

var buffer1 = new Buffer("this is the content of my buffer");
var buffer2 = new Buffer(11);
var targetStart = 0;
var sourceStart = 8;
var sourceEnd = 19;
buffer1.copy(buffer2,targetStart, sourceStart, sourceEnd);

将源缓冲区的第8到第19个位置上的数据复制到目标缓冲区的起始位置。

缓冲区解码

var str = buf.toString();
var b64Str = buf.toString("base64");

小结

有时候你不得不对二进制数据进行处理,native JS没提供方法,Node中的Buffer类对访问连续内存块的操作进行了封装,可以处理内存中的数据 以及切分缓冲区,还可以在两个缓冲区之间进行内存复制。

使用事件发射器模式简化事件绑定

理解标准回调模式

后续传递风格:一种程序的流程控制权以后继的形式被显式传递的编程风格。

var fs = require('fs');
fs.readFile('/etc/passwd',function(err,fileContent){
  if(err){
  throw err;
  }
  console.log('file content',fileContent.toString());
})

在这个例子中,将一个内联匿名函数传递给fs.readFile函数。作为其第二个参数,其实这就是在使用CPS(continuation-passing style),因为 在这个内联匿名函数将函数执行继续了下去。

理解事件发射器模式

在标准回调模式中,将一个回调函数作为参数传递给将被执行的函数,这种模式在函数执行结束后需要通知客户端的情况下工作得很好。但是如果在函数执行过程中发生多个事件或者事件反复发生了多次,那这种模式就工作得不那么好了。如果要求在套接字上每当的有可用数据时都能收到通知,标准回调模式就帮不上什么忙了,此时正是事件发射器模式派上用场的时候了。

  • 在使用事件发生器模式时,会涉及两个或者更多的对象,这些对象包括事件发射器以及一个或者多个事件监听器,一对多的关系。
  • 顾名思义,事件发射器就是可以发射事件的对象,而事件监听器则是绑定到事件发射器上的代码,负责监听特定类型的事件。
var req = http.request(options, function(response) {
    response.on("data", function(data) {
        console.log("some data from the response", data);
    });
    response.on("end", function() {
    console.log("response ended");
});
});
req.end();

在上面的例子中,可以看到几个必要的步骤,Node的http.request API创建了一个HTTP请求,并将其发送到远程服务器。第一行用到了后继传递风 格,传递了一个当HTTP服务器响应时就会被调用的内联函数。 一般而言,当需要在请求的操作完成后重新获取控制权时就使用CPS(Continuation Passing Style)模式,当事件可能发生多次时就使用事件发射器模式。

理解事件类型

请注意发射的事件具有类型,类型用一个字符串表示,前面例子中有“data”和“end”两种事件类型,它们都是由事件发射器定义的任意字符串,不过 约定俗成的是,命名通常都是由不包含空格的小写单词组成。 你无法通过编程来判断事件发射器到底能够发射哪些类型的事件,因为事件发射器API没有提供内省机制,因此用到的api应该有文档来记录它能发射的事件类型。 一旦有相关事件发生,事件发射器就会调用相应的事件监听器,并将相关数据作为参数传递给事件监听器。 下面代码模拟了一个能发射两种类型事件的事件发射器:

var em = new (require(‘events’).EventEmitter)();
em.emit('event1');
em.emit('error',new Error('My mistake'));

随便发射一个名为“event1”的事件没有任何效果,但是在发射“error”事件时,错误就会被抛出到堆栈。如果程序不是运行在REPL命令行环境中, 就会因为这个未捕获到的错误而挂起。一般而言,应该总是监听错误事件,并对其进行恰当的处理。

应用事件发生器API

任何实现了事件发射器模式的对象(比如TCP套接字、HTTP请求等),都实现了如下所示的一组方法:

  • .addListenr和.on —— 为指定类型的事件添加事件监听器。
  • .once —— 为指定类型事件绑定一个仅会被调用一次的事件监听器。
  • .removeEventListener —— 删除绑定到指定事件上的某个指定事件监听器
  • .removeAllEventListener —— 删除绑定到指定事件上的所有事件监听器。
使用.addListener()或.on()绑定回调函数

通过指定事件类型和回调函数,就可以注册当事件发生时所要调用的操作。例如,当有可以用的数据块时,文件可读就会发射“data”事件,下面的代码展示如何通过一个回调函数来通知发生了“data”事件:

function receiveData(data) {
    console.log("got data from file read stream: %j", data);
}
readStream.addListener("data", receiveData);

还可以用函数.on来代替.addListener函数,它只是.addListen函数的简写形式。

绑定多个事件监听器

事件发射器模式允许多个事件监听器监听同一事件发射器的同一类型的事件:

readStream.on("data", function(data){
  console.log('i have some data here');  
})
readStream.on("data", function(data){
  console.log('i have some data here too');  
})

在上面的代码中,readStream对象的“data”类型事件上绑定了两个函数,每当readStream对象发射“data”事件时,就会看到输出信息:

I have some data here.
I have some data here too.

根据事件类型,事件发射器负责按照事件所绑定的监听器的注册顺序依次调用事件监听器,这意味着以下两件事:

  • 某个事件监听器也许并不会在事件发射之后立即被调用,也许在它之前会有别的事件监听器被调用。
  • 异常被抛出到堆栈并不正常,它通常是由代码中的错误引起的。当事件被发射时,如果其中某个事件监听器在被调用时抛出错误,可能会导致一些事件监听器永远都不会被调用,在这种情况下,事件发射器将会捕捉到错误,也许还会处理它。