深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)

内容

1.Node简介
2.模块机制
3.异步I/O
4.异步编程

一、Node简介

1.Node的特点

  • 异步I/O
  • 事件与回调函数
  • 单线程
    弱点:
    1)无法利用多核CPU
    2)错误会引起整个应用退出,应用的健壮性值得考验。
    3)大量计算占用CPU导致无法继续调用异步I/O。
  • 跨平台

2.Node的应用场景

  • I/O密集型
  • 是否不擅长CPU密集型业务
  • 与遗留系统和平共处
  • 分布式应用

3.Node的使用者

  • 前后端编程语言环境统一
  • Node带来的高性能I/O用于实时应用
  • 并行I/O使得使用者可以更高效地利用分布式环境
  • 并行I/O,有效利用稳定接口提升Web渲染能力
  • 云计算平台提供Node支持
  • 游戏开发领域。
  • 工具类应用

二、模块机制

1.CommonJs规范

主要分为以下三个部分

  • 模块引用
    require()方法接受模块标识,以此引入一个模块的API到当前上下文中

     var math=require('math');
  • 模块定义
    exports对象用于导出当前模块的方法或变量,并且它是唯一导出的出口。
    module对象,它代表模块本身,exports是module的属性。
    在node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式

    //math.js
    exports.add=function(){
        return 'aaaa'
    }
    // program.js
    var math=require('math');
    var a=math.add();//aaa
  • 模块标识
    模块标识就是传递给require()方法的参数,
    它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或是绝对路径,可以没有文件名后缀.js

2.Node的模块实现

在node中引入模块,需要经历如下三个步骤

  • 路径分析
  • 文件定位
  • 编译执行

模块分为两类:

  • 核心模块:node提供的模块。在node源代码的编译过程中,编译进了二进制执行文件,加载速度快。
  • 文件模块:用户编写的模块。运行时动态加载,速度比较慢。

1)优先从缓存中加载

node 对引入过的模块都会进行缓存,以减少二次引入时的开销,缓存的是编译和执行之后的对象
require()方法对相同的模块二次加载都一律采用缓存优先的方式,核心模块的检查先于文件模块的缓存检查

2)路径分析和文件定位

  • 模块标识符分析:
    加载速度:核心模块 > 路径形式的文件模块 > 自定义模块
  • 文件定位:
    文件扩展名分析:如果标识符中不包含文件扩展名,则按.js 、.json 、.node 的次序补足扩展名。
    目标分析和包:如果没有查找到对应文件,但却得到一个目录,此时node会将目录当做一个包来处理

3)模块编译

对于不同的文件扩展名,载入方法不同,具体如下:

  • .js文件:通过fs模块同步读取文件后编译执行
  • .node文件:这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
  • .json文件:通过fs模块同步读取文件后,用JSON.parse()解析返回结果
  • 其余扩展文件。它们都被当做.js文件载入

require.extensions:获取系统中已有的扩展加载方式。

编译方式:

  • JavaScript模块的编译:
    在编译过程中,对js内容进行了包装
    执行之后,exports属性被返回了调用方,被外部调用

    (function(exports,require,module,__filename,__dirname){
        // 内容
        var math=require('./math');
        var a=math.add();
    })
  • C/C++模块的编译
    调用dlopen()方法进行加载和执行
  • JSON文件的编译
    通过fs模块同步读取文件后,用JSON.parse()得到对象,然后将它赋给模块对象的exports,以供外部调用

3.核心模块

核心模块分为两部分:C/C++编写(文件放在Node项目的src目录下),JavaScript编写(文件放在Node项目的lib目录下)。

1)JavaScript核心模块的编译过程

  • 转存为C/C++代码:将所有内置的JavaScript代码转换成C++里的数组
  • 编译JavaScript核心模块:进行头尾包装,与文件模块的区别是:获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置。

2)C/C++核心模块的编译过程
内建模块:纯C/C++编写的部分,通常不被用户直接调用。eg:buffer,crypto,evals,fs,os等模块。

  • 内建模块的组织形式
    优势:它们由C/C++编写,性能上优于脚本语言;在文件编译时,被编译成二进制文件,一旦Node开始执行,它们被直接加载进内存中,无须再次做标识符定位、文件定位、编译等过程,直接就可执行。
  • 内建模块的导出
    在加载内建模块时,我们先创建一个exports空对象,然后调用get_builtin_module()方法取出内建模块对象,通过执行
    register_func()填充对象,最后将exports对象按模块名缓存,并返回给调用方法完成导出。

3)核心模块的引入流程
os原生模块的引入流程
NODE_MODULE(node_os,reg_func) => get_builtin_module('node_os') => process.binding('os') => NativeModule.require('os') => require('os')

4)编写核心模块
编写内建模块通常分两步:编写头文件和编写C/C++文件。

4.C/C++扩展模块

C/C++扩展模块属于文件模块中的一类。

5.模块调用栈

C/C++内建模块是最底层的模块,属于核心模块,主要提供API给Javascript核心模块和第三方Javascript文件模块调用,实际中几乎不会接触到此类模块。Javascript核心模块主要职责有两种:一种是作为C/C++内建模块的封装层和桥接层供文件模块调用,另一种是纯粹的功能模块,不需要跟底层打交道。文件模块通常由第三方编写,包括普通Javascript模块和C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第1张图片

6.包与NPM

包由两部分组成:包结构和包描述文件。前者用于组织包中的各种文件。后者用于描述包的相关信息,以供外部读取分析。
1)包结构
包本质上是一个存档文件(一般为.zip或.tar.gz),安装后解压还原为目录。完全符合CommonJS规范的包目录应包含以下文件:

  • .package.json:包描述文件
  • .bin:存放可执行二进制文件的目录
  • .lib:存放Javascript代码的目录
  • .doc:存放文档的目录
  • .test:存放单元测试用例的代码

2)包描述文件与NPM
包描述文件是一个JSON文件——package.json,位于包的根目录下,是包的重要组成部分,用于描述包的概况信息。
而NPM的所有行为都与这个文件的字段息息相关。

下面将以知名Web框架express项目的package.json文件为例说明一些常用字段的含义。

  • name:包名
  • description:包简介
  • version:版本号,需遵照“语义化的版本控制”,参照http://semver.org/
  • keywords:关键词数组,NPM主要用来做分类搜索。
  • contributors:贡献者列表
  • dependencies:使用当前包所需要依赖的包列表。NPM会通过这个属性自动加载依赖的包
  • repositories:托管源代码的位置列表
  • engine:支持的JavaScript引擎列表
  • scripts:脚本说明对象,它主要被包管理器用来安装编译测试和卸载包

NPM与包规范的区别在于多了以下四个字段

  • author:包作者
  • bin:可以作为命令行工具使用
  • main:模块引入方法require()在引入包时,将其作为包中其余模块的入口。
  • devDependencies:一些模块只在开发时需要依赖,配置这个属性,可以提示包的后续开发者安装依赖包

3)NPM常用功能

安装依赖包:

  • 全局模式安装:npm install -g 通过全局安装的所有模块包都被安装进了一个统一的目录下,
    但并不意味着可以从任何地方通过require()来引用到它。
  • 本地安装 :将包下载到本地,然后以本地安装。本地安装只需为npm指明package.json文件所在的位置即可。
  • 从非官方源安装:可以通过镜像源安装 eg:npm install underscore --registy=http://registy.url

NPM包管理
下面以grunt-cli(grunt命令行工具)为例,列出常用的包管理命令:

  • npm install:安装package.json文件的dependencies和devDependencies字段声明的所有包
  • npm install [email protected]:安装特定版本的grunt-cli
  • npm install grunt-contrib-copy --save:安装grunt-contrib-copy,同时保存该依赖到package.json文件
  • npm uninstall grunt-cli:卸载包
  • npm list:查看安装了哪些包
  • npm publish :发布包

7.前后端共用模块

// 兼容多种模块规范,让同一个模块可以运行在前后端
(function(name,definition){
    // 检测上下文环境是否为AMD或CMD
    var hasDefine = typeof define === 'function',
        // 检测上下文环境是否为Node
        hasExports = typeof module !=='undefined' && module.exports; 
    if(hasDefine){
        // AMD或CMD环境
        define(definition);
    }else if(hasExports){
        // 定义为普通Node模块
        module.exports[name] = definition();
    }else{
        // 将模块的执行结果挂在window变量中
        this[name] = definition();
    }
    
})('hello',function(){
    var hello = function(){};
    return hello;
})

三、异步I/O

1.为什么要异步I/O

1)用户体验
javascript在单线程上执行,它与UI线程是一个线程,
如果使用同步,当javascript在执行的时候UI渲染和响应处于停滞状态,用户的体验极差。

// 现在请求两个资源
//消费时间是M
getData('from_db');
//消费时间是N
getData('from_remote_api');

如果是同步,需要耗时(M + N);
如果是异步,需要耗时Max(M, N);

随着应用的复杂性,情景会变成M+N+...和Max(M,N,...),此时同步和异步的优劣就会更加凸显。
另一方面,随着网站和应用的扩展,数据往往会分布到多台服务器上,而分布意味着M和N的值会线性增长,这也会放大异步和同步在性能上的差异。总之,IO是昂贵的,分布式IO是更昂贵的!

2)资源分配

Node利用单线程,远离多线程死锁、状态同步等问题,利用异步I/O,让单线程远离阻塞,使得CPU得到更好的利用。
为了弥补单线程无法利用多核CPU的缺点,Node提供了子进程 childProcess ,该子进程可以通过工作进程高效地利用CPU和I/O。

异步I/O调用示意图

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第2张图片

2.异步I/O实现现状

阻塞IO:阻塞的IO操作就是发起IO操作后,线程阻塞等待IO完成,这期间cpu得不到有效利用。
非阻塞IO:发起IO操作后,通过事件轮巡,或者事件通知机制,不断查询IO操作是否完成,或者是主线程进入休眠等待事件通知IO结束,然后继续向下执行代码,实际上非阻塞IO期间,cpu要么用来查询要么用来休眠,也没有得到有效利用。依旧是同步IO。
Node的异步IO:采用了线程池技术,发起异步IO时,把io操作扔到线程池里面执行,然后主线程继续执行其他操作,io执行完毕通过线程间通信通知主线程,主线程执行回调。
IO线程是由Libuv(Linux下由libeio具体实现;window下则由IOCP具体实现)管理的线程池控制的,本质上是多线程。即采用了线程池与阻塞IO模拟了异步IO。
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第3张图片

3.Node的异步I/O

事件循环、观察者、请求对象、I/O线程池这四者共同构成了Node异步I/O模型的基本要素。
在Node中,除了js是单线程外,node自身其实是多线程的,只是I/O线程使用的CPU较少。
除了用户代码无法并行执行外,所有的I/O则是可以并行起来的。

1)事件循环
当I/O线程上的任务(阻塞I/O)执行完毕之后,就会产生一个事件,这就是事件循环中的事件的产生由来。
node进程启动的时候,会创建一个类似的 while(true) 循环,每执行一次循环体的过程被称为Tick。每个Tick的过程就是查看是否有事件待处理。
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第4张图片

2)观察者
Node中事件的主要来源是网络请求和文件IO等。这些事件对应的观察者就是网络I/0观察者、文件I/0观察者。
事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者。这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

3)请求对象
从js发起调用到内核执行I/O完的过程中,存在一种中间产物,称为是请求对象。

以打开文件为例:
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第5张图片

0. 异步调用任务
1. js调用核心模块
2. 核心模块调用C++内建模块
3. 内建模块在`libuv`层,分平台处理。实质上调用的都是`uv_fs_open`方法。
4. 在调用的过程中,创建一个`FSReqWrap`请求对象。【这就是我们的主角请求对象了】
5. 对象创建完毕后,设置好参数和回调函数,就会将其推入线程池中等待执行了。
6. js线程继续执行后续的任务,当前的IO操作在线程池中执行,不管IO线程上是阻塞还是非阻塞,都不会影响主线程的执行,因此这就达到了异步的目的了。

3)执行回调
当IO线程中的任务执行完毕后,就会将执行结果放在请求对象中。然后通知IOCP。IOCP检查任务是否完成。如果完成了就将I/O请求对象加入观察者队列中,当作事件处理。然后通过事件循环来执行回调函数。

整个异步I/O的流程:
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第6张图片

4.非I/O的异步API

1)定时器
定时器(setTimeout(),setInterval())的实现原理同异步IO,只是没有使用线程池。
调用时创建的定时器会被加入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,若超过则形成一个事件,其回调函数立即执行。
定时器的问题在于,它并非精确的,尽管事件循环十分快,但有可能某次Tick执行时间比较长。

2)process.nextTick()
将回调函数放入到队列中,在下一轮Tick时取出执行,可以达到setTimeout(fn,0)的效果,由于不需要动用红黑树,效率更高时间复杂度为O(1)。

3)setImmediate()
将回调函数延迟执行,process.nextTick()中的回调函数执行的优先级要高于setImmediate()。
由于事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者,
优先级:idle观察者>I/O观察者>check观察者

process.nextTick()的回调函数保存在数组中,每次Tick会将数组中的回调函数全部执行;
setImmediate()的回调函数保存在链表中,每次Tick只执行链表中的一个回调函数(旧版本)。每次Tick链表中的回调函数全部执行(新版本)

process.nextTick(function(){
    console.log('nextTick延迟执行1');
});
process.nextTick(function(){
    console.log('nextTick延迟执行2');
});

setImmediate(function(){
    console.log('setImmediate延迟执行1');
    process.nextTick(function(){
        console.log('nextTick延迟执行3');
    });
});
setImmediate(function(){
    console.log('setImmediate延迟执行2');
});
console.log('正常执行');

// 正常执行
// nextTick延迟执行1
// nextTick延迟执行2
// setImmediate延迟执行1
// setImmediate延迟执行2
// nextTick延迟执行3

5.事件驱动与高性能服务器

事件驱动的实质,即通过主循环加事件触发的方式来运行程序。

利用Node构建Web服务器的流程图:
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第7张图片

经典服务器模型:

  • 同步式:一次只能处理一个请求,并且其余请求都处于等待状态。
  • 每进程/每请求:为每个请求启动一个进程,这样可以处理多个请求,但是它不具备扩展性,因为系统资源只有那么多。
  • 每线程/每请求:为每个请求启动一个线程来处理。尽管线程比进程要轻量,但是由于每个线程都要占用一定内存,当大并发请求到来时,内存将
    会很快用完,导致服务器缓慢。目前被Apache采用。
  • Node通过事件驱动的方式处理请求,无须为每个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为
    线程较少,上下文切换的代价很低。这使服务器能够有条不紊地处理请求,即使在大量连接的情况下,也不受线程上下文切换开销的影响,这是
    Node高性能的一个原因。

四、异步编程

1.函数式编程

1)高阶函数
高阶函数则是可以把函数作为参数,或是将函数作为返回值的函数。

//例如 sort()方法,接收一个方法作为参数
var arr=[1,10,3,9];
arr.sort(function(a,b){
    return a-b;
});
console.log(arr);//[ 1, 3, 9, 10 ]

2)偏函数用法
偏函数用法是指创建一个调用另外一个部分————参数或变量已经预置的函数————的函数的部分。

// isType函数可以批量创建一些类似的函数,通过这个函数预先指定type的值,然后返回一个新的函数。
var isType=function(type){
    return function(obj){
        console.log(Object.prototype.toString.apply(obj))
        return Object.prototype.toString.apply(obj) == '[object '+type+']';
    }
}
var isString=isType('String');
var isFunction=isType('Function');

2.异步编程的优势与难点

1)优势
node的优势在于基于事件驱动的非阻塞IO模型,这个模型使得非阻塞IO可以使cpu计算与IO相互解耦,让资源得到更好的利用。

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第8张图片

分解任务的方法来应对cpu密集型的程序:
由于事件循环模型需要应对海量请求,海量请求同时作用在单线程上,就需要防止任何一个计算耗费过多的cpu时间片。至于是计算密集型,还是IO密集型,只要计算不影响异步IO的调度,那就不构成问题。建议对cpu的耗用不要超过10ms,或者将大量的计算分解为诸多的小量计算,通过setImmediate()进行调度。只要合理利用node的异步模型与V8的高性能,就可以充分发挥cpu和IO资源的优势。

2)难点

难点 描述 解决
异常处理 无法利用try/catch/final的方式捕获异常,也就是说对于回调抛出的异常,使用传统的同步异常抓取办法是抓不到的。 将回调函数的第一个实参作为err回传,如果为null则没有异常,如果有err对象,则发生了异常。这也就要求我们在写异步程序时,第一,要有回调函数,第二,要正确设置回调函数的参数,并且将第一个参数设置为err,第三,要确保在回调函数内部发生错误时正确的传递了这个错误。
函数嵌套过深 callback hell 使用async/await来将异步变同步
阻塞代码 因为node是单线程程序,因此没有sleep()来阻塞程序 使用setTimeout()来阻塞程序,但是这个方案也未必就好,阻塞代码的做法,不要在node中出现,尽量还是利用异步事件编程,来实现业务。
多线程 node的js执行方式是单线程的 node没有web workers,同时,web workers虽然解决了利用cpu和减少阻塞ui渲染的问题,但是还是不能解决ui渲染效率的问题。因此,在node层面,使用了child_process作为基础的解决API方案,同时还提供了cluster模块作为更深层次的应用解决方案。
异步转同步 嵌套回调,业务分散 使用async/await来将异步变同步
// 异常处理的正确参数传递
var async=function(callback){
    process.nextTick(function(){
        var results=somthing;
        if(error){
            return callback(error);
        }
        callback(null,results);
    })
}

浏览器提高了web workers来将js执行和ui渲染分离,并通过web workers的消息传递来调度多核cpu进行运算。

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第9张图片

3.异步编程解决方案

1)事件发布/订阅模式
事件发布/订阅模式,其实就是回调函数的事件化。这个功能基于的是node自身提供的events模块。
这个events模块提供了addListener/on()、once()、removeListener()、removeAllListeners()、emit()等基本的事件监听模式的方法实现。

订阅事件就是一个高阶函数的应用,事件发布/订阅模式可以实现一个事件与多个回调函数的关联,这些回调函数又被称为事件监听器。

//订阅
emitter.on("event1", function (message) {
    console.log(message);
});
// 发布
emitter.emit('event1', "I am message!");

1.继承events模块
实现一个继承EventEmitter的类,在node核心模块中,几乎有近一半的模块都继承自EventEmitter。

var events = require('events');
var util = require('util');
function Stream() {
    events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);

2.利用事件队列解决雪崩问题
雪崩问题,是因为高访问量和大并发的情况下,造成缓存失效,大量的请求同时涌入数据库中,使得数据库无法同时承受如此大的查询需求,从而影响整个网站整体的响应速度。

我们可以利用事件队列解决雪崩问题。利用once()方法,使得通过它添加的侦听器只能执行一次,在执行之后就会将他与事件的关联移除。这个特性可以帮助我们过滤一些重复的事件响应。
此处可能会存在侦听器过多引发的警告,需要调用setMaxListeners(0),移除警告,或者设置更大的警告阈值。

var proxy = new events.EventEmitter();
var status = "ready";
var select = function (callback) {
    proxy.once("selected", callback);
    if (status === "ready") {
        status = "pending";
        db.select("SQL", function (results) {
            proxy.emit("selected", results);
            status = "ready";
        });
    }
};

3.多异步之间的协作方案
在异步编程中,会出现事件与侦听器的关系是多对一的情况。

通过node的原生代码,来解决callback hell的问题,这里以渲染页面需要的模板读取、数据读取和本地化资源读取为例。

方案1: 利用发布订阅方式,来完成多对多的方案:

var after = function (times, callback) {
    var count = 0, results = {};
    return function (key, value) {
        results[key] = value;
        count++;
        if (count === times) {
            callback(results);
        }
    };
};
var emitter = new events.Emitter();
var done = after(times, render);
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) {
    emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
    emitter.emit("done", "data", data);
});
l10n.get(function (err, resources) {
    emitter.emit("done", "resources", resources);
});

方案2:EventProxy的模块,它是对事件发布订阅模式的扩充,可以自由订阅组合事件。

EventProxy提供了一个all()方法来订阅多个事件,当每个事件都被触发后,侦听器才会被执行。
使用tail()方法,在满足条件时执行一次之后,如果组合事件中的某个事件被再次触发,侦听器会用最新的数据继续执行。
after()方法,可以实现事件在多少次访问后,执行。

var proxy = new EventProxy();
proxy.all("template", "data", "resources", function (template, data, resources) {
    // TODO
});
fs.readFile(template_path, "utf8", function (err, template) {
    proxy.emit("template", template);
});
db.query(sql, function (err, data) {
    proxy.emit("data", data);
});
l10n.get(function (err, resources) {
    proxy.emit("resources", resources);
});

4.EventProxy的原理

EventProxy来自于Backbone的事件模型。
EventProxy则是将all当做一个事件流的拦截层,在其中注入一些业务来处理单一事件无法解决的异步处理问题。类似的扩展方法还有all、tail、after、not、any

5.EventProxy的异常处理
根据commonjs的规范,异常处理都被封装在了回调函数的第一个err中。

exports.getContent = function (callback) {
    var ep = new EventProxy();
    ep.all('tpl', 'data', function (tpl, data) {
        // 成功回调
        callback(null, {
            template: tpl,
            data: data
        });
    });
    //绑定错误处理函数
    ep.fail(callback);
    fs.readFile('template.tpl', 'utf-8', ep.done('tpl'));
    db.get('some sql', ep.done('data'));
};

2)Promise/Deferred模式
利用Promise/Deferred模式来先执行异步调用,延迟传递处理内容。
Promise/Deferred模式在CommonJS下抽象出了Promises/A、Promises/B、 Promises/D等模式
1.Promises/A
Promise/Deferred模式包含Promise模式和Deferred模式两部分。
Promise对单个异步操作的抽象定义如下:
1.Promises只会存在三种状态,未完成态、完成态、失败态
2.状态只会从未完成态向完成态,或者从未完成态向失败态转化,过程不可逆,完成态和失败态也不会相互转化。
3.状态一旦转化,将不能被更改。

Promise的状态转化示意图:

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第10张图片

Promises/A的实现非常简单,一个Promises对象只需要具备then()方法即可,这个then()有如下特点:

1.接受完成态、错误态的回调方法,在操作完成或者出现错误时,将会调用对应方法。
2.可选的支持progress事件回调作为第三方法
3.then()方法只接受function对象,其余对象将被忽略。
4.then()方法继续返回promise对象,以实现链式调用。

//then()方法的定义
then(fulfilledHandler, errorHandler, progressHandler)

//使用events模块来实现then()
var Promise = function () {
    EventEmitter.call(this);
};
util.inherits(Promise, EventEmitter);
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) {
    if (typeof fulfilledHandler === 'function') {
        this.once('success', fulfilledHandler);
    }
    if (typeof errorHandler === 'function') {
        this.once('error', errorHandler);
    }
    if (typeof progressHandler === 'function') {
        this.on('progress', progressHandler);
    }
    return this;
};

实现then()方法所做的事情,是将回调函数存放起来,为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象通常被称为Deferred,即延迟对象,示例代码如下:

var Deferred = function () {
    this.state = 'unfulfilled';
    this.promise = new Promise();
};
Deferred.prototype.resolve = function (obj) {
    this.state = 'fulfilled';
    this.promise.emit('success', obj);
};
Deferred.prototype.reject = function (err) {
    this.state = 'failed';
    this.promise.emit('error', err);
};
Deferred.prototype.progress = function (data) {
    this.promise.emit('progress', data);
};

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第11张图片

//实现promise
var promisify = function (res) {
    var deferred = new Deferred();
    var result = '';
    res.on('data', function (chunk) {
        result += chunk;
        deferred.progress(chunk);
    });
    res.on('end', function () {
        deferred.resolve(result);
    });
    res.on('error', function (err) {
        deferred.reject(err);
    });
    return deferred.promise;
};

//执行代码

promisify(res).then(function () {
// Done
}, function

深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第12张图片
// Error

}, function (chunk) {
// progress
console.log('BODY: ' + chunk);
});

deferred主要用于内部,用于维护异步模型状态,promise则作用于外部,通过then()方法,暴露给外部已添加自定义逻辑。
![图片上传中...]

第三方包:Q
Q是Promise/A规范的一个实现,通过npm install q 安装。

defer.prototype.makeNodeResolver = function () {
    var self = this;
    return function (error, value) {
        if (error) {
            self.reject(error);
        } else if (arguments.length > 2) {
            self.resolve(array_slice(arguments, 1));
        } else {
            self.resolve(value);
        }
    };
};

//如果基于q则变为:

var readFile = function (file, encoding) {
var deferred = Q.defer();
fs.readFile(file, encoding, deferred.makeNodeResolver());
return deferred.promise;
};
readFile("foo.txt", "utf-8").then(function (data) {
// Success case
}, function (err) {
// Failed case
});

2.promise中的多异步协作

因为promise主要是用来解决单个异步操作中存在的问题,那么多个异步调用处理如下:

Deferred.prototype.all = function (promises) {
    var count = promises.length;
    var that = this;
    var results = [];
    promises.forEach(function (promise, i) {
        promise.then(function (data) {
            count--;
            results[i] = data;
            if (count === 0) {
                that.resolve(results);
            }
        }, function (err) {
            that.reject(err);
        });
    });
    return this.promise;
}

var promise1 = readFile("foo.txt", "utf-8");
var promise2 = readFile("bar.txt", "utf-8");
var deferred = new Deferred();
deferred.all([promise1, promise2]).then(function (results) {
// TODO
}, function (err) {
// TODO
});

3.Promise的进阶知识
Promise的秘诀其实在于队列的操作。
支持序列执行的Promise
如果让promise支持链式执行,需要以下两个步骤:
1.将所有的回调都存入队列中
2.promise完成时,逐个执行回调,一旦检测到返回了新的promise对象,就停止执行,然后将当前deferred对象的promise引用改变为新的promise对象,并将队列中余下的回调转交给它。

将API promise化

// smooth(fs.readFile);
var smooth = function (method) {
    return function () {
        var deferred = new Deferred();
        var args = Array.prototype.slice.call(arguments, 1);
        args.push(deferred.callback());
        method.apply(null, args);
        return deferred.promise;
    };
};

var readFile = smooth(fs.readFile);
readFile('file1.txt', 'utf8').then(function (file1) {
    return readFile(file1.trim(), 'utf8');
}).then(function (file2) {
    // file2 => I am file2
    console.log(file2);
});

3)流程库控制

1.尾触发与next
除了事件和Promise外,还有一类方法是需要手工调用才能持续执行后续调用的,我们将此类方法叫做尾触发
常见的关键词是next,目前应用最多的地方是Connect的中间件。

var app = connect();
// Middleware
app.use(connect.staticCache());
app.use(connect.static(__dirname + '/public'));
app.use(connect.cookieParser());
app.use(connect.session());
app.use(connect.query());
app.use(connect.bodyParser());
app.use(connect.csrf());
app.listen(3001);

通过use()方法注册号一系列中间件后,监听端口上的请求。中间件利用了尾触发机制

function (req, res, next) {
// 中间件
}

每个中间件传递请求对象,响应对象和尾触发函数,通过对了形成一个处理流。
深入浅出nodeJS -1 - (Node简介、模块机制、异步I/O、异步编程)_第13张图片

// connect的核心实现
function createServer() {
    function app(req, res) { app.handle(req, res); }//创建http服务器的request事件处理函数
    utils.merge(app, proto);
    utils.merge(app, EventEmitter.prototype);
    app.route = '/';
    app.stack = [];
    for (var i = 0; i < arguments.length; ++i) {
        app.use(arguments[i]);
    }
    return app;
};
// stack属性是这个服务器内部维护的中间件队列,通过调用use(),我们可以将中间件放入队列
app.use = function (route, fn) {
    // some code
    this.stack.push({ route: route, handle: fn });
    return this;
};

2.async

  • 异步的串行执行:series()
  • 异步的并行执行:parallel()
  • 异步调用的依赖处理:waterfall()
  • 自动依赖处理:auto()

3.step
setp比async更加轻量,通过npm install step安装即可使用。step只有一个接口:Step(task1, task2, task3);
它可以接受任意数量的任务,所有的任务都会串行依次执行

step的this关键字,是他内部的next()方法,将异步调用的结果传递给下一个任务做为参数。
Step(
    function readFile1() {
        fs.readFile('file1.txt', 'utf-8', this);
    },
    function readFile2(err, content) {
        fs.readFile('file2.txt', 'utf-8', this);
    },
    function done(err, content) {
        console.log(content);
    }
)

并行任务执行:this.parallel()
结果分组:group()
4.wind

wind基于任务模型实现
// 定义了异步任务
eval(Wind.compile('async',function(){}))
// 内置了对setTimeout的封装
Wind.Async.sleep()
// 实现等待完成异步方法
await()
方法 说明
事件发布/订阅 node的事件底层实现,是其他库的实现基础,比较原始和底层,理解后对于其他库的原理可以有更深刻的理解
promise/deferred 它是一种解决异步编程的规范,并做了代码抽象和封装,现在已经广泛应用于各种异步库中。
eventproxy 对于events模块的扩展,可以理解其原理,深刻体会流程控制的精妙之处
async 流程控制库,可以解决异步串行、并行、自动执行等多种任务
step 流程控制库
wind 流程控制库
streamline 流程控制库

4.异步并发控制

在node中,我们十分方便利用异步发起并行调用,但是如果并发量过大,我们的下层福取钱将会吃不消。
如果对文件系统进行大量并发调用,操作系统的文件描述符数量将会被瞬间用光,抛出错误:Error: EMFILE, too many open files
所以,需要给予一定的过载保护,以防止过犹不及。

1)bagpipe的解决方案
bagpipe可以实现通过队列控制并发量的功能,同时还启用了拒绝模式,防止大量的异步调用。另外,对于过长时间的异步调用,也提供了超时控制。
bagpipe的实现思路:

  • 通过一个队列来控制并发量
  • 如果当前活跃(指调用发起但未执行回调)的异步调用量小于限定值,从队列中取出执行
  • 如果活跃用量达到限定值,调用暂时存放在队列中
  • 每个异步调用结束时,从队列中取出新的异步调用执行

2)async的解决方案

  • parallelLimit()用于处理异步调用的限制,不可以动态添加并行任务。
  • queue()实现动态添加并行任务,但是接收的参数是固定的。

你可能感兴趣的:(node.js)