-
一、简介- 单线程:
- 优点:不用想多线程编程那样处处在意状态的同步问题,没有死锁也没有线程上下文交换所带来的性能上的开销。
- 缺点:1.无法利用多核cpu,2.错误会引起整个应用退出,应用的健壮性值得考验,3.大量计算占用cpu导致无法继续调用异步I/O。
- 解决方案: child_process。子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核cpu方面的问题。通过将计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果。这可以很好的保持应用模型的简单和低依赖。
- 应用场景:
- I/O密集型:I/O密集的优势主要在于Node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少。
- cpu密集型:V8的深度性能优化使得node有优秀的运算能力,足够高效。
- cpu密集型应用给node的主要挑战是:单线程遇到长时间的运行计算(比如大计算),将会导致cpu的时间片不能释放,使得后续I/O无法发起。这时需要分解大型运算任务为多个小任务,对于纯计算,node虽然没有提供多线程用于计算支持,但可以通过编写c/c++扩展的方式更高效的利用cpu。甚至可以通过子进程的方式,将一部分node进程当作常驻服务进程用于计算,然后利用进程间的消息来传递结果,将计算与I/O分离。主要是合理调度。(这段还是有点迷)
- 使用者:
- 前后端编程语言环境统一
- node带来的高性能I/O用于实时应用:长链接,socket
- 并行I/O→ 分布式环境:高效的使用已有的数据
- 并行I/O → 有效利用稳定接口提升web渲染能力:抛弃同步等待式的顺序请求,并行I/O,加速数据的获取进而提升web的渲染速度。
- 云计算平台提供node支持
- 游戏开发:实时和高并发。
- 工具类
二、模块机制- 实现Commonjs规范
- Node模块实现:路径分析;文件定位;编译执行。
- Node提供的模块为核心模块,用户编写的为文件模块,不是路径形式的标识符是一种特殊的文件模块,查找最费时(树形向上),加载最慢。
- 核心模块部分在Node源码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略,并在路径分析中优先判断,所以他的加载速度是最快的。
- 文件模块在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度慢。
- 缓存编译和执行后的对象。
- 路径分析
- require 会按.js\.json\.node的次序不足扩展名,依次尝试,因此.node 和 .json最好带上扩展名。其次同步配合缓存,可以大幅度缓解node单线程中阻塞式调用的缺陷。
- 当require通过分析文件扩展名后,没有找到对应的文件,却找到了一个目录,会将该目录当作一个包来处理:
- 在当前目录下查找package.json,通过JSON.parse解析出包描述对象,从中找出main属性指定的文件名进行定位。
- 如果main属性指定的文件名错误,或者压根没有package.json,Node会将index当作默认文件名。
- 模块编译
- 定位到具体的文件后,Node会新建一个模块对象,根据路径载入并编译。
- 载入方式:
- .js文件: fs模块同步读取文件后编译执行。
- .node文件:dlopen()方法加载最后编译生成的文件。
- .json文件:通过fs模块同步读取文件后,用JSON.parse解析返回结果。
- 其余扩展名文件:被当作.js处理。
- 编译方式:
- js:在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加 了(function (exports, require, module, __filename, __dirname) {\n,然后在尾部添加了\n});。因此每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval, 只是具有明确的上下文,不污染全局。)返回一个具体的function对象。最后,将当前模块对象的exports属性,require方法,module以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。⚠️:如果直接复制给exports通常会得到一个失败的结果,因为exports对象是通过形参的方式传入的,直接赋值给形参会改变形参的引用,但并不能改变作用域外的值。如果要达到require引入一个类的效果,请赋值给module.exports对象。
- c/c++:因为.node模块是编写c/c++模块之后编译生成的,所以不需要编译,在这个过程中只有加载和执行。在执行过程中,模块的export是对象与.node模块产生联系,然后返回给调用者。
- json:通过fs模块同步读取文件后,用JSON.parse解析得到对象,将它赋值给模块对象的exports,供外部调用。
- 核心模块 :c/c++存在src目录下,js存在lib目录下。
- js核心模块编译过程
- 转存为c/c++代码:以字符串的形式存在node命名空间中,是不可直接执行的。在启动Node进程时,js代码直接加载进内存中。在加载的过程中,js核心模块经历标识符分析后直接定位到内存中,比普通文件在磁盘中查找要快很多。
- 编译js核心代码:lib下的所有模块文件也没有require\module\exports等。在引入js核心模块过程中,也经历了头尾包装等过程,然后才执行和导出exports对象。与文件模块的区别在于:
- 获取源码方式: 核心模块的源文件通过process.binding('natives')从内存中取出,文件模块自磁盘取出
- 缓存的位置不同:核心模块缓存到NativeModule._cache对象上,文件模块缓存到Module._cache对象
- 编译c/c++核心模块:
- 由纯c/c++编写的部分称为内建模块。例如:buffer、crypto、evals、fs、os等。
- 用结构体形式定义一个内建模块,通过NODE_MODULE宏将模块定义到node命名空间,模块的具体初始化方法挂载为结构的register_func成员。 然后把散列的内建模块统一放进node_module_list数组中。
- 在加载内建模块时:先创建一个exports空对象,调用get_builtin_module()方法取出内建模块对象,通过执行register_func()填充exports对象,然后将export是对象按模块名缓存,并返回给调用放完成导出。
- 核心模块的引入流程 图2-5
- js核心模块编译过程
- c/c++扩展模块(迷,略)
- 模块调用栈 图 2-8
-
包与npm 图2-9
-
- 包结构
- pakage.json 包描述文件
- ---------必需的字段-----------
- name: 包名 小写字母数字._-
- description:包简介
- version:版本号 major.minor.revision
- keywords:关键词数组,用于分类搜索
- maintainers:包维护者列表 "maintainers": [{ "name": "Jackson Tian", "email": "shyvo1987@gmail.com", "web": "http://html5ify.com" }]
- ---------------权限认证属性-------------------
- contributors:贡献者列表
- bugs:一个可以反馈bug的网页地址或邮箱地址。
- licenses:当前包使用的许可证列表,表示这个包可以在那些许可证下使用。例"licenses": [{ "type": "GPLv2", "url": "http://www.example.com/licenses/gpl.html", }]
- repositories。托管源代码的位置列表
- dependencies。使用次前包所需要依赖的包列表
- ---------------可选-------------------
- homepage\os\cpu\engine\builtin\directories\implements\
- scripts: 脚本说明对象 "scripts": { "install": "install.js", "uninstall": "uninstall.js", "build": "build.js", "doc": "make-doc.js", "test": "test.js" }
- ---------------NPM实际需要的字段----------
- name、version、description、keywords、 repositories、author、bin、main、scripts、engines、dependencies、devDependencies
- author:作者;bin:包可以作为命令行工具使用;main:包引入时最先检查的模块入口,没有就index;devDependencies:开发依赖
- bin 存放可执行二进制文件的目录
- lib 存放js代码的目录
- doc 存放文档的目录
- test 存放单元测试用例的代码
- pakage.json 包描述文件
- 安装本地模块:npm install <tarball file>
- 非官方源安装:npm install underscore --registry=http://registry.url
- 发布包:用时再查
- 包结构
-
- 前后端共用模块
- 模块的侧重点: 为后端js制定的规范commonjs几乎全是同步引入模块,这并不适用前端,因此出现AMD、CMD等规范
- AMD:是commonjs模块规范的一个延伸。define(id?, dependencies?, factory); factory的内容就是实际代码内容。例: define(['dep1', 'dep2'], function (dep1, dep2) { return function () {};});
- AMD模块需要用define来明确定义一个模块(而node实现中是隐式包装的),目的是进行作用域隔离,仅在需要时被引入。避免掉过去那种通过全局变量或全局命名空间的方式,以避免变量污染和不小心被修改。另一个区别则是内容需要通过返回的方式实现导出。
- CMD:与AMD规范的主要区别在于定义模块和依赖引入的部分。AMD需要在声明模块的时候指定所有的依赖,通过形参传递到模块内容中。在依赖部分,cmd支持动态引入,例: define(function(require, exports, module) { // the module code goes here });
- 兼容多种模块规范:
- 单线程:
三、异步I/O
与node的事件驱动、异步I/O设计理念比较相近的是nginx。nginx为纯c编写,性能优异,他们的区别为:nginx具备面向客户端管理连接的强大能力,但它的背后依然受限于各种同步方式的编程语言。但node却是全方位的,既可以作为服务器端去处理客户端带来的大量并发请求,也能作为客户端向网络中的各个应用进行并发请求。
- 为什么异步I/O :
- 用户体验: 快
- 资源分配:node利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好的利用cpu。异步I/O的提出是期望I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余需要的业务去执行。
- 异步I/O实现现状:
- 各种轮询。。(不是很懂)
- node异步I/O
- 事件循环:在进程启动时,node会创建一个类似于while(true)的循环,每执行一次循环体的过程成为tick。
- 观察者:判断每个tick过程是否有事件需要处理。每个事件循环中有一个或多个观察者,判断是否有事件要处理的过程就是向这些观察者询问是否又要处理的事件。
事件可能来自用户的点击或加载某些文件时产生,而这些产生的事件都有对应的观察者。在node中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者,网络I/O观察者等。观察者将事件进行了分类。事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。 - 请求对象:从js发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,就是请求对象。
创建一个FSReqWrap对象,把参数和当前方法以及回调函数多挂在这个对象上。然后根据不同平台调用不同方法将FSReqWrap推入线程池中等待执行。例如windows:QueueUserWorkItem()接受3个参数:1.将要执行的方法的引用。2.时1方法运行时所需要的参数。3.执行的标志。当线程池有可用线程时回调用1.。至此,js调用立即返回,由js层面发起的异步调用的第一阶段就此结束。js线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到js线程的后续操作,如此就达到了异步的目的。 - 执行回调:组装好请求对象,送入I/O线程池等待执行,实际上完成了异步I/O的第一部分,回调通知是第二部分。
线程池中的I/O操作调用完毕后,会将结果存储在req→ result属性上,然后调用某poststatus方法通知事件循环(windows基于IOCP,*nix基于多线程),告知当前对象操作已完成(具体:提交执行状态,将线程归还线程池)。在这个过程中还动用了事件循环的I/O观察者。在每次tick执行时,它会调用某getstatus方法提取poststatus方法提交的状态,从而检查线程池中是否有执行完的请求。如果有,会将请求对象加入到I/O观察者的队列中,然后将其当作事件处理。I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出回调方法,然后调用执行。以此达到调用js传入的回调函数的目的。
- 事件循环:在进程启动时,node会创建一个类似于while(true)的循环,每执行一次循环体的过程成为tick。
- 非I/O的异步API
- 定时器:setTimeout、setInterval 实现原理与异步I/O比较类似,只是不需要I/O线程池的参与。调用时创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。
问题:它并非精确的(在容忍范围内)。尽管事件循环十分快,但是如果某次循环占了太多时间,那么下次循环时,它也许超时很久了。比如通过setTimeout()设定一个任务在10ms后执行,但是9ms后,有个任务占了5ms的cpu时间片,再次轮到定时器执行时,时间就已经过期4ms。 - process.nextTick() : 功能类似于setTimeout( , 0); 但是无需进行定时器的红黑树查询。更高效。
- setImmediate():优先级低于process.nextTick() ,因为事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmdiate()属于check观察者。idle > I/O > check。
在具体实现上,process.nextTick的回调函数保存在一个数组中,setImmediate结果保存在链表中。行为上,process.nextTick在每轮循环中会将数组中的回调函数全部执行完,而setImmediate会在每轮循环中执行链表中的一个回调函数。
- 定时器:setTimeout、setInterval 实现原理与异步I/O比较类似,只是不需要I/O线程池的参与。调用时创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数将立即执行。
- 事件驱动与高性能服务器
- 几种典型服务器模型:
- 同步式:一次只能处理一个请求。并且其余请求都处于等待状态。
- 每进程/每请求:为每个请求启动一个进程,这样可以处理多个请求,但是他不具备扩展性,因为系统资源只有那么多。
- 每线程/每请求:为每个请求启动一个线程来处理。尽管线程比进程要轻量,但是由于每个进程都要占用一定内存,当大并发请求到来时,内存将会很快用光,导致服务器缓慢。apache
- node通过事件驱动的方式处理请求,无须为每个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较低,上下文切换的代价也很低。
- nginx使用相同的事件驱动,但采用纯c写成,虽性能较高,但仅适合于做web服务器,用于反向代理或负载均衡等服务,处理具体业务方面较为欠缺。
- 几种典型服务器模型:
四、异步编程
- 函数式编程:函数(function) 是js中的一等公民。
- 高阶函数:把函数作为参数,或是将函数作为返回值的函数。
高阶函数比普通函数灵活的多,除了通常意义的函数调用返回外,还形成了一种后续传递风格(continuation passing style)的结构接收方式,而非单一的返回值形式,后续传递风格的程序编写将函数的业务重点从返回值转移到了回调函数中。例如sort就是经典的高阶函数: arr.sort(function(a, b) { return a - b; }); forEach map reduce reduceRight filter every some等都是高阶函数 - 偏函数用法:创建一个调用另外一个部分——参数或变量已经预置的函数——的函数的用法。isType(String)这种通过指定部分参数来产生一个新的定制函数的形式就是偏函数。
- 高阶函数:把函数作为参数,或是将函数作为返回值的函数。
- 异步编程的优势与难点
- 优势引发的问题:js线程像一个分配任务和处理结果的大管家,I/O线程池里的各个I/O线程都是小二,负责兢兢业业地完成分配来的任务,小二与管家之间互不依赖,所以能保持整体的高效率。缺点是管家无法承担过多的细节性任务,如果承担太多,则会影响到任务的调度,管家忙个不停,小二却得不到活干,结局则是整体效率的降低。换言之,node是为了解决编程模型中阻塞I/O的性能问题的,采用了单线程模型,这导致node更像一个处理I/o密集问题的能手,而cpu密集型则取决于管家的能力。如果node在V8性能的基础上借助c/c++能力可逼近顶尖。
解决原则:由于事件循环模型需要对应海量请求,海量请求同时作用在单线程上,就需要防止任何一个计算耗费过多的cpu时间片。至于是计算密集型,还是I/O密集型,只要计算不影响异步I/O的调度,就不构成问题。建议对cpu的耗用不要超过10ms,或将大量的计算分解成诸多的小量计算,通过setImmediate进行调度。 - 难点:
- 异常处理:在调用异步函数的外面trycatch只能捕捉到提交请求部分,处理结果的异常捕捉不到。
node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明异步调用没有异常抛出。
还有一些自行编写异步方法时的原则:- 必须执行调用者传入的回调函数;
- 正确传递回异常供调用者判断;
- 不能对用户传递的回调函数进行异常捕获,否则回调回执行两次。也就是不在try中调用callback。而是在catch中callback(err),在try和catch外callback()。
- 函数嵌套过深 :写的时候难写,也没有利用好异步I/O带来的并行优势。
- 阻塞代码:js没有sleep()这样的线程沉睡功能。唯独能用于延时操作的只有setInterval()和setTimeout()这两个函数。不能用while(new Date() - start < 1000) 来当作sleep功能。因为由于node单线程的原因,cpu资源全都会用于为这段代码服务,导致其余任何请求都会得不到响应。这样的需求,用setTimeout()会更好。
- 多线程编程:node借鉴了web workers的模式,child_process是其基础api,cluster模块是更深层次的应用。
- 异步转同步:有些没有同步api比较麻烦,可能不能得到原生支持,需要借助库或者编译等手段来实现。
- 异常处理:在调用异步函数的外面trycatch只能捕捉到提交请求部分,处理结果的异常捕捉不到。
- 优势引发的问题:js线程像一个分配任务和处理结果的大管家,I/O线程池里的各个I/O线程都是小二,负责兢兢业业地完成分配来的任务,小二与管家之间互不依赖,所以能保持整体的高效率。缺点是管家无法承担过多的细节性任务,如果承担太多,则会影响到任务的调度,管家忙个不停,小二却得不到活干,结局则是整体效率的降低。换言之,node是为了解决编程模型中阻塞I/O的性能问题的,采用了单线程模型,这导致node更像一个处理I/o密集问题的能手,而cpu密集型则取决于管家的能力。如果node在V8性能的基础上借助c/c++能力可逼近顶尖。
- 异步编程解决方案
- 事件发布/订阅模式:事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式。
- node提供events模块,是发布/订阅模式的一个简单实现。这个模块比前端浏览器的dom事件简单,不存在事件冒泡、preventDefault、stopPropagation和stopImmediatePropagation等控制时间传递的方法。它具有addListener/on、once、removeListener、removeAllListeners和emit等基本的事件监听模式的方法实现。
//订阅
emitter.on("event1", function(message) {
console.log(message);
})
//发布
emitter.on(“event1”, "I am message!");
此模式可以实现一个事件与多个回调函数的关联,这些回调函数又称为事件侦听器。通过emit发布事件后,消息会立即传递给当前事件的所有侦听器执行。侦听器可以灵活的添加和删除,使得事件和具体处理逻辑之间可以很轻松的关联和解耦。模式自身并无异步和同步调用的问题,但在node中,emit调用多半是伴随事件循环而异步触发的,所以说事件发布/订阅广泛应用于异步编程。
通过该模式进行组件封装。将不变的部分封装在组件内部,将容易变化、需要自定义的部分通过事件暴露给外部处理,从某种角度来说事件的设计就是组件的接口设计。从另一个角度看,事件侦听器模式也是一种钩子机制,利用狗子导出内部数据或状态给外部的调用者。node中的很多对象大数具有黑盒的特点,功能点较少,如果不通过事件钩子的形式,我们就无法获取对象在运行期间的中间值或内部状态。这种通过事件钩子的方式,可以是编程者不用关注组件是如何启动和执行的,只需关注在需要的事件点上即可。例如在http请求的代码中,只需要将视线放在error、data、end这些业务事件点上即可,内部流程无需过多关注。
基于健壮性考虑,node对事件发布/订阅的机制做了一下额外的处理。- 如果一个事件超过10个侦听器,会得到一条警告。设计者认为侦听器太多可能导致内存泄漏,所以提出警告。调用emitter.setMaxListeners(0);可以去掉限制。另外,由于事件发布会引起一系列侦听器执行,如果事件相关的侦听器过多,可能存在过多占用cpu的情景。
- 为了处理异常,EventEmitter对象对error事件进行了特殊对待。如果运行期间的错误触发了error事件,EventEmitter会检查是否对error 事件添加过侦听器。如果有会将错误交给侦听器处理。否则作为异常抛出。如果外部没有捕获这个异常,将会引起线程退出。建议对error事件做处理
- 特性
- 继承events模块:实现一个继承EventEmitter的类:Stream对象继承EventEmitter的例子:
var events = require('events');
function Stream() {
events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter); //node在util模块中封装了继承的方法,此处可以便利的调用。 - 利用事件队列解决雪崩问题:在事件订阅/发布模式中,也有个once()方法,通过它添加的侦听器只能执行一次,在执行后就会将它与事件的关联移除。可以帮我们过滤一些重复性的事件响应。
-
雪崩问题: 在计算机中,缓存由于存放在内存中,访问速度十分快,常常用于加速数据访问,让绝大多数的请求不必重复去做一些低效的数据请求。雪崩问题就是在高访问量、大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承受如此巨大的查询请求,进而往前影响到网站整体的响应速度。
一条数据库查询语句的调用:
var select = function (callback) {
db.select("SQL", function (result) {
callback(results);
});
} //此时如果站点刚好启动,这是缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,会影响服务的整体性能。 -
一种改进方案是添加一个状态锁:
var status = "ready";
var select = function (callback) {
if (status === "ready") {
status = "pending";
db.select("SQL", function (results) {
status = "ready";
callback(results);
});
}
}; //在这种情况下,连续地多次调用select()时,只有第一次调用时生效的,后续但select()是没有数据服务的。 -
这个时候可以引入事件队列:
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";
});
}
}; //这里我们利用了once方法,将所有请求的回调都压入事件队列中,利用其执行一次就会将监视器移除的特点,保证每一个回调只会被执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的过程中永远只有一次。SQL在进行查询时,新到来的相同调用只需在队列中等待数据就绪即可,一旦查询结束,得到的结果可以被这些调用共同使用。这种方式能节省重复的数据库调用产生的开销。由于Node单线程执行的原因,此处无须担心状态同步的问题。这种方式其实也可以应用到其他远程调用的场景中,即使外部没有缓存策略,也能有效节省重复开销。此处可能因为存在侦听器过多引发的警告,需要调用setMaxListeners(0)移除掉警告,或者设更大的警告阈值。
-
- 多异步之间的协作方案
- 背景:事件发布/订阅模式有着它的优点。利用高阶函数的优势,侦听器作为回调函数可以随意添加和删除,它帮助开发者轻松处理随时可能添加的业务逻辑。也可以隔离业务逻辑,爆出业务逻辑单元的职责单一。一般而言,事件与侦听器的关系是一对多,但在异步编程中,也会出现事件与侦听器的关系是多对一的情况,也就是说一个业务逻辑可能依赖两个通过回调或事件传递的结果。前面提及的回调嵌套过深的原因即是如此。
- 尝试用原生代码解决“难点2”中为了最终结果的处理而导致可以并行调用但实际只能串行执行的问题。以渲染页面所需要的模版读取、数据读取和本地化资源读取为例简要介绍:
- 由于多个异步场景中回调函数的执行并不能顺序,且回调函数之间互相没有任何交集,所以需要借助一个第三方函数和第三方变量来处理异步协作的结果。通常,我们把这个用于检测次数的变量叫做哨兵变量。可以利用偏函数来处理哨兵变量和第三方函数的关系。
- 上述方案实现了多对一的目的。如果业务继续增长,我们可以继续利用发布/订阅方式来完成多对多的方案:
- 另一个方案——EventProxy模块:对事件订阅/发布模式对扩,可以自由订阅组合事件。EventProxy提供了一个all方法来订阅多个事件,当每个事件都被触发之后,侦听器才会执行。另外的一个方法是tail方法。它与all方法的区别在于all的侦听器在满足条件之后只会执行一次,tail方法的侦听器则在满足条件时执行一次之后,如果组合事件中的某个事件再次被触发,侦听器会用最新的数据继续执行。all方法带来的另一个改进是:在侦听器中返回的数据的参数列表与订阅组合事件的事件列表是一致对应的。
除此之外,在异步的场景下,我们常常需要从一个接口多次读取数据,此时触发的事件名或许是相同的。EventProxy提供了after方法来实现事件在执行多少次后执行侦听器的单一事件组合订阅方式。示例:
var proxy = new EventProxy();
proxy.after("data", 10, function (datas) {
//TODO
}); //这段代码表示执行10次data事件后执行侦听器。这个侦听器得到的数据为10次按事件触发次序排序的数组
- EventProxy的原理:将all当作一个事件流的拦截层,在其中注入一些业务来处理单一事件无法解决的异步处理问题。类似的扩展方法还有all、tail、after、not、any等。
- EventProxy的异常处理:
- 旧版:
- 改进:
- fail:
- done:
- done传参:当只传入一个回调函数时,需要手工调用emit()触发事件
- done传参:同时传入事件名和回调函数
- 旧版:
- Promise/Deferred模式:使用事件的方式时,执行流程需要被预先设定。这是由发布/订阅模式的运行机制决定的。而Promise/Deferred模式即使不调用sucess error等方法,也能执行ajax等异步操作。
- Promises/A:一个Promise对象只要具备then()方法即可。then 的的方法定义: then(fulfilledHandler, errorHandle, progressHandler).对于then有以下要求:
- 接收完成态、错误态的回调方法。在操作完成或出现错误时,将会调用对应方法。
- 可选的支持progress事件回调作为第三个方法。
- 只接受function参数,其余对象将被忽略。
- 继续返回Promise对象,以实现链式调用。
- Promises/A:一个Promise对象只要具备then()方法即可。then 的的方法定义: then(fulfilledHandler, errorHandle, progressHandler).对于then有以下要求:
- 继承events模块:实现一个继承EventEmitter的类:Stream对象继承EventEmitter的例子:
- node提供events模块,是发布/订阅模式的一个简单实现。这个模块比前端浏览器的dom事件简单,不存在事件冒泡、preventDefault、stopPropagation和stopImmediatePropagation等控制时间传递的方法。它具有addListener/on、once、removeListener、removeAllListeners和emit等基本的事件监听模式的方法实现。
- 事件发布/订阅模式:事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式。