内容
1.Node 简介 2. 模块机制 3. 异步 I /O4. 异步编程 5. 内存控制 6. 理解 Buffer7. 网络编程 8. 构建 Web 应用 9. 玩转进程 10. 测试 11. 产品化
一、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 <package-name> -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 grunt-cli@0.1.9:安装特定版本的 grunt-cli
npm install grunt-contrib-copy –save:安装 grunt-contrib-copy,同时保存该依赖到 package.json 文件
npm uninstall grunt-cli:卸载包
npm list:查看安装了哪些包
npm publish <folder>:发布包
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 请求对象加入观察者队列中,当作事件处理。然后通过事件循环来执行回调函数。
整个异步 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. 事件驱动与高性能服务器
事件驱动的实质,即通过主循环加事件触发的方式来运行程序。
利用 Node 构建 Web 服务器的流程图:
经典服务器模型:
同步式:一次只能处理一个请求,并且其余请求都处于等待状态。
每进程 / 每请求:为每个请求启动一个进程,这样可以处理多个请求,但是它不具备扩展性,因为系统资源只有那么多。
每线程 / 每请求:为每个请求启动一个线程来处理。尽管线程比进程要轻量,但是由于每个线程都要占用一定内存,当大并发请求到来时,内存将 会很快用完,导致服务器缓慢。目前被 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/APromise/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
// Error
}, function (chunk) {
// progress
console.log(‘BODY: ‘ + chunk);
});
deferred 主要用于内部,用于维护异步模型状态,promise 则作用于外部,通过 then()方法,暴露给外部已添加自定义逻辑。![图片上传中 …]
第三方包:QQ 是 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.stepsetp 比 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()实现动态添加并行任务,但是接收的参数是固定的。