内容
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模块调用扩展模块。
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调用示意图
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。
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的过程就是查看是否有事件待处理。
2)观察者
Node中事件的主要来源是网络请求和文件IO等。这些事件对应的观察者就是网络I/0观察者、文件I/0观察者。
事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者。这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。
3)请求对象
从js发起调用到内核执行I/O完的过程中,存在一种中间产物,称为是请求对象。
0. 异步调用任务
1. js调用核心模块
2. 核心模块调用C++内建模块
3. 内建模块在`libuv`层,分平台处理。实质上调用的都是`uv_fs_open`方法。
4. 在调用的过程中,创建一个`FSReqWrap`请求对象。【这就是我们的主角请求对象了】
5. 对象创建完毕后,设置好参数和回调函数,就会将其推入线程池中等待执行了。
6. js线程继续执行后续的任务,当前的IO操作在线程池中执行,不管IO线程上是阻塞还是非阻塞,都不会影响主线程的执行,因此这就达到了异步的目的了。
3)执行回调
当IO线程中的任务执行完毕后,就会将执行结果放在请求对象中。然后通知IOCP。IOCP检查任务是否完成。如果完成了就将I/O请求对象加入观察者队列中,当作事件处理。然后通过事件循环来执行回调函数。
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.事件驱动与高性能服务器
事件驱动的实质,即通过主循环加事件触发的方式来运行程序。
经典服务器模型:
- 同步式:一次只能处理一个请求,并且其余请求都处于等待状态。
- 每进程/每请求:为每个请求启动一个进程,这样可以处理多个请求,但是它不具备扩展性,因为系统资源只有那么多。
- 每线程/每请求:为每个请求启动一个线程来处理。尽管线程比进程要轻量,但是由于每个线程都要占用一定内存,当大并发请求到来时,内存将
会很快用完,导致服务器缓慢。目前被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相互解耦,让资源得到更好的利用。
分解任务的方法来应对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进行运算。
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的状态转化示意图:
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);
};
//实现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
}, 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) {
// 中间件
}
每个中间件传递请求对象,响应对象和尾触发函数,通过对了形成一个处理流。
// 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()实现动态添加并行任务,但是接收的参数是固定的。