在本章,我们将继续学习设计模式,着重了解行为型设计模式。我们在第5章所学的创建型设计模式侧重于对象的创建,在第6章所学的结构型设计模式侧重于对象结构,而本章介绍的行为型设计模式则侧重于辅助实现代码库中的多个对象之间的通信。本章的要点是要使得我们更容易去从整体的角度理解代码是如何在一起运作的,而不是着重于单独的对象的创建与结构。让我们一起学习8种行为型设计模式。
7.1 职责链模式
当基于一个相同“类”的若干个类可以相应地处理某项请求或调用时,可以使用职责链(chain of responsibility)模式。该项请求首先会发送至一个对象,如果该对象不是能处理该请求的最合适对象,则会把该请求传递至另一个对象进行处理。这样,传递就会不断进行,直至达到可以处理该请求的对象。然后,通过此职责链各对象的操作处理所得的结果会返回给原来的请求或方法调用。职责链中的每个对象都认识其他一个对象。如果一个对象不能亲自处理到达的请求,就会由职责链中的下一继任的对象来处理。当多个对象在一起时能组合成某种类型的层级关系(如代码请求7-1所示),但我们又不希望向代码的其他部分暴露其中的实现过程,则此时使用职责链模式最为合适。
代码清单7-1 职责链模式
1 //定义一个对象,列出系统中日志记录的不同级别————普通消息、警告消息和错误消息。每一种级别相比于其上一种级别,所表示的请求更为严重 2 var LogLevel = { 3 INFO: 'INFO', 4 WARN: 'WARN', 5 ERROR: 'ERROR' 6 }, 7 log; 8 //定义一个“类”,用于为不同日志级别的日志消息生成相应的格式化日志消息 9 function LogFormatter(logLevel) { 10 this.logLevel = logLevel; 11 } 12 LogFormatter.prototype = { 13 //定义一个属性,用于保存此对象实例在职责链中的后继者 14 nextInChain: null, 15 //定义一个方法,用于设置此对象实例在职责链中的后继者 16 setNextInChain: function(next) { 17 this.nextInChain = next; 18 }, 19 //定义一个方法,基于当前的日志记录级别生成相应格式的日志消息 20 createLogMessage: function(message, logLevel) { 21 var returnValue; 22 23 //如果当前对象实例被指派的日志记录级别与传入的参数相同,则格式化该日志信息 24 if(this.logLevel === logLevel) { 25 //根据日志记录级别来相应地格式化该日志消息 26 if(logLevel === logLevel.ERROR) { 27 returnValue = logLevel + ":" + message.toUpperCase(); 28 } else if(logLevel === logLevel.WARN) { 29 returnValue = logLevel + ":" + message; 30 } else { 31 returnValue = message; 32 } 33 //如果当前对象实例所被指派的日志记录级别与传入的参数不相同,则把该消息传递至职责链的下一个对象实例 34 } else if(this.nextInChain) { 35 returnValue = this.nextInChain.createLogMessage(message, logLevel); 36 } 37 return returnValue; 38 } 39 }; 40 //定义一个单例,用于保存和输出系统的日志消息记录 41 log = (function() { 42 //定义一个存储数组以存放日志消息 43 var logs = [], 44 //创建3个对象实例,分别表示3种级别的日志记录:普通消息、警告消息和错误消息 45 infoLogger = new LogFormatter(LogLevel.INFO), 46 warnLogger = new LogFormatter(LogLevel.WARN), 47 errorLogger = new LogFormatter(LogLevel.ERROR), 48 logger = errorLogger; 49 //使用每个对象事例中的setNextInChain()方法来设置职责链的层级。我们假设“错误信息”最为重要,其位于职责链的首位 50 //在日志级别层级中,“错误消息”的下一位是“警告消息”,因为它的重要性次之 51 errorLogger.setNextInChain(warnLogger); 52 //在日志级别层级中,“警告消息”的下一位时“普通消息”,因为它的重要性最为普通 53 warnLogger.setNextInChain(infoLogger); 54 return { 55 //定义一个方法,用于读取所保存的日志消息 56 getLogs: function() { 57 return logs.join(" "); 58 }, 59 //定义一个方法,用于根据消息的日志记录级别把消息进行相应的格式化 60 message: function(message, logLevel) { 61 //我们只需调用层级中的首位对象实例的createLogMessage()方法。如果该方法本身不能处理该特定日志级别, 62 //则会进一步按职责链依次调用相应的方法。该日志消息会向职责链进一步传递,直至日志消息到达了能够处理该日志级别的对象实例 63 var logMessage = logger.createLogMessage(message, logLevel); 64 //添加该经格式化的日志消息至存储数组 65 logs.push(logMessage); 66 } 67 } 68 }()); 69 70 //执行log单例的message()方法,传入一个消息以及日志级别。职责链中的首位对象处理的是“错误消息”级别的日志消息, 71 //因此以下日志消息不会向职责链进一步下传,日志消息由errorLogger对象返回 72 log.message("Something vary bad happened", LogLevel.ERROR); 73 //因为errorLogger对象只能处理“错误消息”级别的日志消息,所以以下消息会通过职责链进过errorLogger对象传至warnLogger对象 74 log.message("Something bad happened", LogLevel.WARN); 75 //以下消息会经过errorLogger对象传至warnLogger对象,并继续传至infoLogger对象,该对象可以处理“普通消息”级别的日志消息 76 log.message("Something happened", LogLevel.INFO); 77 78 //输入所存储的日志记录 79 console.log(log.getLogs()); 80 //输出结果 81 /* 82 *Something vary bad happened 83 *Something bad happened 84 *Something happened 85 */
当需要访问一些有层级关系的对象,但又不想向代码的其余部分暴露此层级结构时,使用职责链模式最为合适。
7.2 命令模式
通过确保所有的调用都经过某个对象上的一个单独、公共的方法(通常命名为run()或execute())发出,命令(command)模式可以为发出调用的代码与某个对象的特定方法之间提供一个抽象层。使用此模式可以更改底层代码和API,但不会影响那些发起调用的代码。
代码清单7-2展示了一个使用命令模式的简单例子,它向一个单独的execute()方法传入所要执行的方法的名称以及执行时所用的参数。
代码清单7-2 命令模式
1 var cookie=(function(){ 2 var allCookies=document.cookie.split(";"), 3 cookies={}, 4 cookiesIndex=0, 5 cookiesLength=allCookies.length, 6 cookie; 7 for (;cookiesIndex<cookiesLength;cookiesIndex++) { 8 cookie=allCookies[cookiesIndex].split("="); 9 cookies[unescape(cookie[0])]=unescape(cookie[1]); 10 } 11 return { 12 get:function(name){ 13 return cookies[name]||""; 14 }, 15 set:function(name,value){ 16 cookies[name]=value; 17 document.cookie=escape(name)+"="+escape(value); 18 }, 19 remove:function(name){ 20 //通过从表示cookie的对象中移除相应名称的cookie的值并设置其失效日期为以前的时间来移除该cookie 21 delete cookie[name]; 22 document.cookie=escape(name)+"=;expires=Thu,01 Jan 1970 00:00:01 GMT;"; 23 }, 24 //提供一个execute()方法,用于对其他方法的调用进行抽象。这样,其他方法的名称就可以在日后于需要时进行变更, 25 //但又不影响代码其余部分使用该API(只要这个execute()继续存在) 26 execute:function(command,params){ 27 //该command参数包含着所要执行的方法的名称,因此要检查该方法的存在以及它是否是函数 28 if(this.hasOwnProperty(command)&&typeof this[command]==="function"){ 29 //如果该方法存在并且可被执行,则传入所提供的params来执行它 30 return this[command].apply(this,params); 31 } 32 } 33 } 34 }()); 35 //使用该execute()方法来非直接地调用cookie单例的set()方法,并提供参数,将其传入到set()方法中, 36 //从而完成一个cookie的设置 37 cookie.execute("set",["name","wing"]); 38 39 //通过使用execute()执行get方法来检查该cookie是否已经正确设置 40 alert(cookie.execute("get",["name"]));//wing
命令模式还可以使用在那些需要有undo(回撤)功能的应用程序的环境中,当中所执行的语句可能需要在将来某个时刻进行反操作,例如,在进行文字处理的网页应用程序的环境中。在这种情况下,命令会被传至一个执行命令的对象。
该对象根据此抽象概念(可回撤的操作)来保存相应的函数(第2个参数),反操作所传入的方法调用(第1个参数)。如代码清单7-3所示,当中展示了一个简单的命令执行对象以及一个结合代码清单7-2的代码来操作cookie的例子。
代码清单7-3 执行命令的对象(利用它可以在网页应用程序中实现多次“回撤”操作)
1 //创建一个单例,它可以执行其他方法并具有“回撤”这些方法的功能 2 var command = (function() { 3 //创建一个数组,用于一次存储“回撤”命令。这样的数组通常也成为做stack(栈)(后进先出) 4 var undoStack = []; 5 return { 6 //定义一个方法,来执行所提供的第1个函数参数,保存第2个函数参数,以便以后对第1个函数的操作执行“回撤”操作 7 execute: function(command, undoCommand) { 8 if(command && typeof command === "function") { 9 //如果第1个参数是函数,则执行该函数,并把第2个参数加入栈中,以备以后对该命令实施反操作 10 command(); 11 undoStack.push(undoCommand); 12 } 13 }, 14 //定义一个方法,利用存储“回撤”命令的栈来对最后一个执行的命令进行反操作 15 undo: function() { 16 //从栈中移除并保存所执行的最后一个命令,即最近一次加入栈的那个命令。以下操作将从栈中移除该命令,使数组的项数减少 17 var undoCommand = undoStack.pop(); 18 if(undoCommand && typeof undoCommand === "function") { 19 //检查该命令是否为一个有效的函数。若是,则执行它,执行的效果实际上就是“回撤”最近一次所执行的命令 20 undoCommand(); 21 } 22 } 23 } 24 }()); 25 26 //把每一项的可被“回撤”的操作包装在对command.execute()方法的调用中,马上执行的命令作为第1个参数传入, 27 //对该命令进行反操作所执行的函数作为第2个参数传入。第2个参数将会被保存起来,直至需要使用它的时刻 28 command.execute(function() { 29 //使用代码清单7-2中的代码来设置一个cookie,它将会马上执行 30 cookie.execute("set", ["name", "wing"]); 31 }, function() { 32 //设置cookie这个动作的反操作作为移除该cookie。此反操作将被保存起来,以供以后使用。 33 //如果调用command.undo()方法,则会进行反操作 34 cookie.execute("remove", ["name"]); 35 }); 36 37 //执行第2个可被“回撤”的操作,设置第2个cookie 38 command.execute(function() { 39 cookie.execute("set", ["company", "baidu"]); 40 }, function() { 41 cookie.execute("remove", ["company"]); 42 }); 43 44 //检查两个cookie的值 45 alert(cookie.get("name")); //wing 46 alert(cookie.get("company")); //baidu 47 48 //把上一次的操作进行反操作,即移除名为company的cookie 49 command.undo(); 50 51 //检查两个cookie的值 52 alert(cookie.get("name")); //"wing" 53 alert(cookie.get("company")); //""(一个空字符串) 因为现在该cookie已经被移除了 54 //对第一次的操作进行反操作,即移除名为name的cookie 55 command.undo(); 56 alert(cookie.get("name")); //"" 57 alert(cookie.get("company")); //""
当需要从代码的其余部分抽象出特定方法的名称时,使用命令模式最为合适。通过方法的名称(以字符串保存)来引用方法,可以在任意时刻更改底层的代码而不影响代码的其余部分。
7.3 迭代器模式
顾名思义,迭代器(iterator)模式可以使应用程序中的代码对一个数据集合进行迭代或循环访问,但又不必清楚该数据在内部是如何保持或构建的。通常,迭代器会提供一组标准方法,用于移动至该集合中的下一个数据项,还用于检查当前数据项是否位于集合中的第一项或最后一项。
代码清单7-4展示的是一个可以迭代数组类型和对象类型数据的一般性“类”的一个例子。通过所提供的rewind()、current()、next()、hasNext()和first()方法,可以实现用手动方式对此迭代器的实例进行操作和查询。还可以使用它的each()方法来实现迭代器的自动迭代访问。each()方法的参数是一个回调函数。对于数组集中的每一项数据,该回调函数都会相应执行一次,从而提供一种十分有用的与for循环等价的访问形式。
代码清单7-4 迭代器模式
1 //定义一个一般性“类”,用于迭代/循环数组或有对象特征(object-like)的数据结构 2 function Iterator(data) { 3 var key; 4 //在data属性中保存所提供的数据 5 this.data = data || {}; 6 this.index = 0; 7 this.keys = []; 8 //使用一个指示符来表示所提供的数据究竟是数组还是对象 9 this.isArray = Object.prototype.toString.call(data) === "[object Array]"; 10 if(this.isArray) { 11 //如果所提供的数据是数组,则保存其length,以供快速访问 12 this.length = data.length; 13 } else { 14 //如果所提供的是对象数据,则把每一项属性的名称保存至数组中 15 for(key in data) { 16 if(data.hasOwnProperty(key)) { 17 this.keys.push(key); 18 } 19 } 20 //该用于保存属性名称的数据组的项数就是对数据进行迭代的数据项数 21 this.length = this.keys.length; 22 } 23 } 24 //定义一个方法,用于重置序号,实际上就是把迭代器重置回数据的起始位置 25 Iterator.prototype.rewind = function() { 26 this.index = 0; 27 }; 28 //定义一个方法,用于返回迭代器当前序号位置保存的值 29 Iterator.prototype.current = function() { 30 return this.isArray ? this.data[this.index] : this.data[this.keys[this.index]]; 31 }; 32 //定义一个方法,用于返回迭代器当前序号位置所保存的值,并将序号增加,指向数据的下一数据项 33 Iterator.prototype.next = function() { 34 var value = this.current(); 35 this.index = this.index + 1; 36 return value; 37 } 38 //定义一个方法,用于指出当前位置是否为数据的末位位置 39 Iterator.prototype.hasNext = function() { 40 return this.index < this.length; 41 } 42 //定义一个方法,用于重置迭代器至数据的起始位置,并返回数据的第一个数据项 43 Iterator.prototype.first = function() { 44 this.rewind(); 45 return this.current(); 46 }; 47 //定义一个方法,用于迭代(或循环)访问每一项数据。每一轮的迭代都会执行回调函数,并把当前数据项作为第1个参数传入该回调函数 48 Iterator.prototype.each = function(callback) { 49 callback = typeof callback === "function" ? callback : function() {}; 50 51 //使用for循环进行迭代,从数据的起始位置开始(通过rewind()方法实现),一直循环,直至再也没有数据供迭代(有hasNext()方法指出) 52 for(this.rewind(); this.hasNext();) { 53 54 //在整个循环过程的每一轮都会执行该回调函数。使用next()方法,把当前数据项的值传入并使循环递增入下一轮 55 callback(this.next()); 56 } 57 }
可以如代码清单7-5所示般来使用代码清单7-4中的代码。这里演示了使用该一般性“类”对所保存的数据进行的不同方式的迭代和循环访问。
代码清单7-5 使用迭代器模式
1 //定义一个对象和一个数组,我们将对它们进行迭代访问 2 var user = { 3 name: "wing", 4 occupation: "Head of web Development", 5 company: "alibaba" 6 }, 7 daysOfWeek = ["Monday", "Tuesday", "wednesday", "Thursday", "Friday", "Saturday", "Sunday"], 8 //使用这2种不同类型的数据来创建该Iterator“类”的2个实例 9 userIterator=new Iterator(user), 10 daysOfWeekIterator=new Iterator(daysOfWeek), 11 //创建3个数组来保存迭代所得的输出数据,以便稍后进行显示 12 output1=[], 13 output2=[], 14 output3=[]; 15 //userIterator已准备就绪可供使用,那么我们就使用for循环来迭代所保存的数据。注意,我们并不需要为for 16 //循环提供第1参数,因为迭代器已经处于重置状态并初始化在数据的起始位置。我们也不需要为for循环提供 17 //第2参数,因为在循环体之外对next()方法的调用为我们实施了对序号位置的增加 18 for (; userIterator.hasNext();) { 19 output1.push(userIterator.next()); 20 } 21 //因为我们迭代的是一个对象,所以所产生的数据是由该对象的每一项属性中所保存的值组成的 22 alert(output1.join(", "));//wing, Head of web Development, alibaba 23 //在再次对同一数据进行迭代之前,迭代器的序号必须倒回至起始位置 24 userIterator.rewind(); 25 //使用while循环来迭代该对象的各项属性,循环会一直进行,直至没有更多的数据供迭代器迭代 26 while(userIterator.hasNext()){ 27 output2.push(userIterator.next()); 28 } 29 alert(output2.join(", "));//wing, Head of web Development, alibaba 30 //使用迭代器的内建each()方法来对数组数据进行迭代。使用这种方法并不需要手动操作序号的位置,只需简单地传入一个回调函数即可 31 daysOfWeekIterator.each(function(item){ 32 output3.push(item); 33 }); 34 alert(output3.join(", "));//Monday, Tuesday, wednesday, Thursday, Friday, Saturday, Sunday
当需要为代码的其余部分提供一种标准的方法来对复杂数据结构进行迭代循环访问,但又不暴露该数据最终是如何保存或表示的时,使用迭代器模式最为合适.
7.4 观察者模式
观察者(observer)模式所使用的场合是,由多个独立代码模块所组成的较大型代码库,各个模块之间需要相互依赖或相互通信。对于这样的一个代码库,从一个模块对另一个模块的硬编码引用就被称为紧解耦,它需要明确系统中的其他每个模块才能使整体代码正确地运行在一起。然而,理想情况是,在大型代码库中的模块应该是松解耦的。引用不能是显式地指向其他模块。相反,要在整个代码库进行全系统性的事件发布与监听,就好比一个自定义版本的标准DOM事件处理。
作为例子,如果一个模块负责处理所有的通过Ajax进行的客户端至服务器的通信,而另一个模块负责渲染和检验表单然后传送至服务器,那么在由用户成功地进行提交并通过验证后,代码库就可以发出一个全局的“表单已提交”事件并传送从表单提交的数据,而通信模块会一直监听此事件。这样,通信模块将会实施它的任务,即把相关数据发送至服务器,并接收服务器的响应数据,然后由通信模块发出“响应已接收”事件,而表单模块则会一直监听此事件。当接收到该事件时,表单模块就会渲染一个消息,指出该表单已经成功提交。这一切都不需要两个模块互相知道对方。每个模块唯一所要清楚的只是一个全局配置的事件名称。该系统中所有的模块都可以发出或响应这些事件。
实现了观察者模式的系统必须具备3个全局方法以供系统代码库使用:publish(),按名称发布一个事件,可传入任意个参数;subscribe(),使某个模块可以指派一个函数,当特定的某名称的事件发生时,执行该函数:unsubscribe(),反指派函数,当相应名称的事件发生时,该函数就不会再被执行。代码清单7-6中的代码演示了一个简单对象,可以在你的应用程序中全局性地使用该对象,以实现观察者模式中的这些方法。
代码清单7-6 观察者模式
10 11 //定义一个包含全局方法publish()、subscribe()和unsubscribe()的对象,用于实现观察者模式 12 var observer = (function() { 13 //定义一个对象,用于按事件名称保存所注册的事件以及和该事件相关联的各个回调函数,以便在全代码库中的任意部分(按名称订阅了这些事件的)使用这些函数 14 var events = {}; 15 return { 16 //定义subscribe()方法,用于保存一个函数以及和该函数相关联的事件名称。稍后某个时刻,当此名称对应的特定事件发出时,调用该函数 17 subscribe: function(eventName, callback) { 18 //如果所提供的名称(第1个参数)对应的事件还有没有被订阅,则在events对象中添加一个属性。 19 //该属性的数据类型为数组,该属性的名称为该事件的名称。以此属性来保存在稍后该名称的事件发出时所要调用的函数 20 if(!events.hasOwnProperty(eventName)) { 21 events[eventName] = []; 22 } 23 //把所提供的回调函数添加至该关联着特定事件名称的数组中 24 events[eventName].push(callback); 25 }, 26 //定义unsubscribe()方法,用于从函数数组(当所提供的名称对应的事件发出时会执行该数组中的函数)中移除一个给定的函数 27 unsubscribe: function(eventName, callback) { 28 var index = 0, 29 length = 0; 30 if(events.hasOwnProperty(eventName)) { 31 length = events[eventName].length; 32 //根据给定的事件名称,循环遍历其对应的数组中所保存的各个函数,并从该数组中移除与所提供的函数(第2个参数)相匹配的函数 33 for(; index < length; index++) { 34 if(events[eventName][index] === callback) { 35 events[eventName].splice(index, 1); 36 break; 37 } 38 } 39 } 40 }, 41 //定义publish()方法,用于依次执行所有与给定的事件名称相关联的所有函数。传给这些函数的参数都是相同的任意项数据, 42 //此数据是作为publish()的参数传入的 43 publish: function(eventName) { 44 //除了第1个参数,把调用publish函数时传入的所有参数保存为一个数组 45 var data = Array.prototype.slice.call(arguments, 1), 46 index = 0, 47 length = 0; 48 if(events.hasOwnProperty(eventName)) { 49 length = events[eventName].length; 50 //根据给定的事件名称,循环遍历其对应的数组中所保存的各个函数,并依次执行这些函数,传入所有提供的参数作为这些函数的参数 51 for(; index < length; index++) { 52 events[eventName][index].apply(this, data); 53 } 54 } 55 } 56 }; 57 }());
代码清单7-7中的代码演示了如何使用代码清单7-6中给出的观察者模式的publish()、subscribe()和unsubscribe()方法。假设这是运行在HTML页面的环境中,页面中含有<form id="my-form">标签,表单拥有有效的action标签特性(指表单可以成功提交给服务器的某个文件),表单中含有若干<input type="text">标签,标示各项表单域。
代码清单7-7 使用观察者模式
1 //定义一个模块,用于Ajax通信。此模块的依赖是代码清单7-6的观察者对象 2 (function(observer) { 3 //定义一个函数,用于进行Ajax POST,所执行的POST基于所提供的URL,form-encoded(表单编码,HTTP/1.1协议中最常见的POST提交数据的方式)的数据字符串, 4 //以及一个回调函数,一旦从服务器成功接收到响应数据就执行回调函数 5 function ajaxPost(url, data, callback) { 6 var xhr = new XMLHttpRequest(), 7 STATE_LOADED = 4, 8 STATUS_OK = 200; 9 xhr.onreadystatechange = function() { 10 if(xhr.readyState !== STATE_LOADED) { 11 return; 12 } 13 if(xhr.status === STATUS_OK) { 14 //一旦从服务器成功接收到响应数据就执行所提供的回调函数 15 callback(xhr.responseText); 16 } 17 }; 18 xhr.open("POST", url); 19 //通知服务器,我们将发送表单编码的数据,这其中的名称和值通过等号(=)进行分隔, 20 //而每个“名称/值”对通过符号(&)进行分隔 21 xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 22 //向服务器POST数据 23 xhr.send(data); 24 } 25 //订阅全局的,自定义的form-submit事件。当该事件由代码库中的其他模块发出时,使用所提供的URL和数据来发出一个Ajax POST请求。完成后,发出ajax-response事件, 26 //并把所提供的URL和数据来发出一个Ajax POST请求。完成后,发出ajax-response事件,并把此次Ajax调用从服务器所得的响应数据传入 27 observer.subscribe("form-submit", function(url, formData) { 28 ajaxPost(url,formData ,function(response) { 29 //发出全局事件ajax-response,把Ajax POST期间从服务器所得到的返回数据传入 30 observer.publish("ajax-response", response); 31 }); 32 }); 33 }(observer)); 34 35 //定义一个模块,用于处理页面中的一个简单表单的提交事宜,此表单含有若干文本表单域,表单的ID是my-form 36 //本代码清单中的两个模块都没有列出对对方的引用,它们只是引用观察者对象。观察者对象负责处理系统中模块之间的所有联系事宜。 37 //每个模块可被称为loosely-coupled(松解耦),因为它们并没有对任何其他模块的硬编码依赖 38 (function(observer) { 39 //获取对当前HTML页面中ID为my-form的表单的引用 40 var form = document.getElementById("my-form"), 41 //从该表单中获取action标签特性的值,这将是我们实施Ajax POST指向的URL 42 action = form.action, 43 data = [], 44 //获取对表单中的所有<input>表单域的引用 45 fields = form.getElementsByTagName("input"), 46 index = 0, 47 length = fields.length, 48 field, 49 //创建一个HTML <p>标签,用作在表单的提交发生之后所显示的感谢信息 50 thankYouMessage = document.createElement("p"); 51 //定义一个函数,在表单提交时执行此函数。此函数使用观察者模式来通过Ajax的方式提交表单域的数据 52 function onFormSubmit(e) { 53 //阻止提交事件的默认行为,这意味着常规的页面内HTML表单提交将不会发生 54 e.preventDefault(); 55 56 //循环遍历表单中的所有<input>标签,把表单中所输入的数据转化成一个由各“名称/值”对所组成的数组 57 for(; index < length; index++) { 58 field = fields[index]; 59 data.push(escape(field.name) + "=" + escape(field.value)); 60 } 61 //用观察者对象发出全局事件form-submit,把Ajax POST指向的URL和所要发送的表单数据传入。 62 //Ajax通信模块正在监听这一事件,并将处理所有与数据提交至服务器相关的事宜 63 observer.publish("form-submit", action, data.join("&")); 64 } 65 //把onFormSubmit()函数关联至表单的submit事件 66 form.addEventListener("submit", onFormSubmit, false); 67 //订阅全局的、自定义的ajax-response事件,并使用随同该事件传来的服务器响应数据来填充感谢消息,以显示在在页面中的表单旁边 68 observer.subscribe("ajax-response", function(response) { 69 thankYouMessage.innerHTML = "Thank you for your form submission.<br>The server responded with:" + response; 70 form.parentNode.appendChild(thankYouMessage); 71 }); 72 }(observer));
利用观察者模式可以移除代码中各模块之间的硬编码引用,以支持实现一个易于维护的自定义的、全系统的事件列表。随着代码库的增长和模块数量的增加,要考虑使用该模式来对代码进行简化,以及实现各模块之间的解耦。请注意,如果某一个模块出现了错误,某个事件没有理多当然地发出,则该错误的根源可能不会立即明显地出现,需要额外的调试。我推荐在开发期间向观察者对象里增加调试日志记录功能,以便更容易地追踪代码中的各种事件。
当希望实现各模块之间的松耦合以减少spaghetti code(意大利面式代码)时,则使用观察者模式最为合适。
7.5 中介者模式
中介者(mediator)模式是观察者模式的一种变体,但二者有个重要差别。观察者模式定义了一个全局性对象用以在整个系统中进行事件的发布与订阅,而中介者模式则定义了一些局部的对象用于实现某些特定的目标,每个对象都具有相同的publish()、subscribe()和unsubscribe()方法。事实证明,随着代码库的不断增大,使用观察者模式会产生出大量难以控制的需要加以管理的事件。因此,可以使用中介者模式来把者较大的事件列表分解划分为较小的多个组别。观察者模式是通过一个全局性的单例对象来实现的,而中介者模式则通过使用一个“类”来实现,因此可以根据需要创建出多个对象实例来支持实现代码的各项功能。代码清单7-8展示了一个可以在代码中实现中介者模式的“类”。注意其与 代码清单7-6中为实现观察者模式所创建的对象之间的相似之处。
1 //定义一个包含着publish()、subscribe()和unsubscribe()方法的“类”,来实现中介者模式。注意其与观察者模式的相似之处。 2 //它们唯一的区别是,这里是创建一个“类”以供以后创建对象实例使用,同时还会为每个对象实例初始化一个用于存储事件的数组,以避免所有实例共享内存中的同一数组 3 function Mediator() { 4 this.events = {}; 5 } 6 Mediator.prototype.subscribe = function(eventName, callback) { 7 if(!this.events.hasOwnProperty(eventName)) { 8 this.events[eventName] = []; 9 } 10 this.events[eventName].push(callback); 11 }; 12 Mediator.prototype.unsubscribe = function(eventName, callback) { 13 var index = 0, 14 length = 0; 15 if(this.events.hasOwnProperty(eventName)) { 16 length = this.events[eventName].length; 17 for(; index < length; index++) { 18 if(this.events[eventName][index] === callback) { 19 this.events[eventName].splice(index, 1); 20 break; 21 } 22 } 23 } 24 }; 25 Mediator.prototype.publish = function(eventName) { 26 var data = Array.prototype.slice.call(arguments, 1), 27 index = 0, 28 length = 0; 29 if(this.events.hasOwnProperty(eventName)) { 30 length = this.events[eventName].length; 31 for(; index < length; index++) { 32 this.events[eventName][index].apply(this, data); 33 } 34 } 35 };
代码清单7-8中的中介者面膜是可以如代码清单7-9所示般进行使用。当中创建了2个中介者对象来表示代码的特定功能,并允许在代码库中实现模块化。假设运行环境为包含有一个<form id="my-form">标签的HTML页面,其中含有若干<input type="text">标签,表示各个表单域。
代码清单7-9 使用中介者模式
1 //为我们的代码库定义2个中介者,一个是编写来辅助实现关于表单的功能的,而另一个是辅助实现日志消息的记录功能的 2 //formsMediator将具备2个事件form-submit和ajax-response,而loggingMediator将具备3个事件log,retrieve和log-retrieved 3 //请注意我们是如何应用中介者模式为不同的功能来划分各个事件的 4 var formsMediator=new Mediator(), 5 loggingMediator=new Mediator(); 6 //定义一个模块,用于Ajax通信。当formsMediator发出form-submit事件时,此模块会把所提供的数据POST至服务器 7 (function(formsMediator){ 8 function ajaxPost(url,data,callback){ 9 var xhr=new XMLHttpRequest(), 10 STATE_LOADED=4, 11 STATUS_OK=200; 12 xhr.onreadystatechange=function(){ 13 if(xhr.readyState!==STATE_LOADED){ 14 return; 15 } 16 if(xhr.status===STATUS_OK){ 17 callback(xhr.responseText); 18 } 19 }; 20 xhr.open("POST",url); 21 xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded"); 22 xhr.send(data); 23 } 24 formsMediator.subscribe("form-submit",function(url,formData){ 25 ajaxPost(url,formData,function(response){ 26 formsMediator.publish("ajax-reponse",response); 27 }) 28 }) 29 }(formsMediator)); 30 31 //定义一个模块,用于处理当前页面的一个简单表单的提交事宜。此表单只包含有若干文本表单域, 32 //表单ID为my-form。当表单提交时,会在formsMediator中发出form-submit事件 33 (function(formsMediator){ 34 var form=document.getElementById("my-form"), 35 action=form.action, 36 data=[], 37 fields=form.getElementsByTagName("input"), 38 index=0, 39 length=fields.length, 40 field, 41 thankYouMessage=document.createElement("p"); 42 function onFormSubmit(e){ 43 e.preventDefault(); 44 for (; index < length; index++) { 45 field=fields[index]; 46 data.push(escape(field.name)+"="+escape(field.value)); 47 } 48 formsMediator.publish("form-submit",action,data.join("&")); 49 } 50 form.addEventListener("submit",onFormSubmit,false); 51 formsMediator.subscribe("ajax-response",function(response){ 52 thankYouMessage.innerHTML="Thank you for your form submission.<br>The server responded with:"+response; 53 form.parentNode.appendChild(thankYouMessage); 54 }); 55 }(formsMediator)); 56 57 //定义一个模块,用于记录该系统中的消息,以帮助调试可能出现的问题,使用loggingMediator来分离出代码库的日志记录功能———与处理表单提交事宜的formsMediator相分离 58 (function(loggingMediator){ 59 //创建一个数组,用于存储日志记录 60 var logs=[]; 61 //当loggingMediator发出log事件时,向logs数组添加一个对象数组项。该对象包含着所提供的消息以及接收到该消息的日期/时间 62 loggingMediator.subscribe("log",function(message){ 63 logs.push({ 64 message:message, 65 date:new Date() 66 }); 67 }); 68 //当loggingMediator发出retrieve-log事件时,这里就发出log-retrieve事件, 69 //并传入当前所保存的logs数组 70 loggingMediator.subscribe("retrieve-log",function(){ 71 loggingMediator.publish("log-retrieve",logs); 72 }); 73 }(loggingMediator)); 74 75 //定义一个模块,使得在loggingMediator中所保存的日志记录在屏幕中显示出来 76 (function(loggingMediator){ 77 //创建一个按钮,单击该按钮时将显示出当前所记录的日志 78 var button =document.createElement("button"); 79 button.innerHTML="Show logs"; 80 button.addEventListener("click",function(){ 81 //用loggingMediator发出retrieve-log事件。这会触发log-retrieve事件。loggingMediator在发出log-retrieve事件时会传入当前所记录的日志 82 loggingMediator.publish("retrieve-log"); 83 },false); 84 //当log-retrieve事件出现时,在屏幕上显示日志记录 85 loggingMediator.subscribe("log-retrieved",function(logs){ 86 var index=0, 87 length=logs.length, 88 ulTag=document.createElement("ul"), 89 liTag=document.createElement("li"), 90 listItem; 91 //循环遍历日志数组中的每条记录,把所保存的日期时间和消息渲染至一个<li>标签中 92 for(;index<length;index++){ 93 listItem=liTag.cloneNode(false); 94 listItem.innerHTML=logs[index].date.toUTCString()+":"+logs[index].message; 95 ulTag.appendChild(listItem); 96 } 97 //在页面下方添加该包含着所有<li>标签的表示当前所记录的日志数据的<ul>标签 98 document.body.appendChild(ulTag); 99 }); 100 //把该按钮添加至当前页面底部 101 document.body.appendChild(button); 102 }(loggingMediator)); 103 104 //定义一个模块,用于记录formsMediator中发生的事件。这是本例中唯一一个使用频率超过一个中介者的模块 105 (function(formsMediator,loggingMediator){ 106 //使用loggingMediator的log事件来记录当form-submit事件在formsMediator中发出时表单所提交的URL 107 formsMediator.subscribe("form-submit",function(url){ 108 loggingMediator.publish("log","Form submitted to"+url); 109 }); 110 //当ajax-response事件在formsMediator中发生时,记录参数中所提供的从服务器返回的响应数据 111 formsMediator.subscribe("ajax-response",function(response){ 112 loggingMediator.publish("log","The server responded to an Ajax call with:"+response); 113 }); 114 115 }(formsMediator,loggingMediator));
随着代码库的增长,你可能会发现从观察者模式转为中介者模式,把系统中的各个事件分组成为更具可维护性的多种功能是很值得做的。
当希望在一个非常大型的代码库中实现各模块之间的松解耦,而若使用观察者模式会使得各种事件的处理变得繁多且难以管理时,使用中介者模式最为合适。
7.6 备忘录模式
备忘录(memento)模式以静态形式在内存中定义对象数据的存储,这样,在代码执行过程的后期就可以对对象数据进行恢复。代码清单7-10展示的是一个简单的“类”,它通过以JSON格式的字符串表示形式来保存对象的快照,并提供方法对原来的JavaScript对象进行保存和恢复,从而实现备忘录模式。
代码清单7-10 备忘录模式
1 //定义一个简单的“类”,用于实现备忘录模式。使用它可以实现保存和恢复内存中的对象快照的功能 2 //某些老式浏览器(例如Internet Explorer7)本身并不支持JSON.stringify()和JSON.parse()。 3 //对于这些浏览器,我们可以引用Douglas Crockford的库,网址为https://github.com/douglascrockford/JSON-js 4 function Memento() { 5 //在内存中定义一个对象直接量,以特定的键(属性)存储其他对象的快照 6 this.storage = {}; 7 } 8 9 //定义一个方式,用于把某对象的状态保存在storage对象直接量的特定键之下 10 Memento.prototype.saveState = function(key, obj) { 11 //把所提供的对象转换成JSON格式的字符串表示形式 12 this.storage[key] = JSON.stringify(obj); 13 }; 14 15 //定义一个方法,用于恢复并返回某特定键之下所保存的对象状态 16 Memento.prototype.restoreState = function(key) { 17 var output = {}; 18 19 //如果所提供的键存在,则找出当中所保存的对象 20 if(this.storage.hasOwnProperty(key)) { 21 output = this.storage[key]; 22 23 //把所保存的值从JSON字符串转换为正规的对象格式 24 output = JSON.parse(output); 25 } 26 return output; 27 };
代码清单7-11演示的是使用了代码清单7-10中的Memento类的一个应用程序。
代码清单7-11 使用备忘录模式
1 //定义一个Memento的实例,用于对目标对象的状态进行保存及恢复 2 var memento = new Memento(), 3 //定义一个对象,我们希望可以对它的状态进行保存及恢复 4 user = { 5 name: "wing", 6 age: 70 7 }; 8 //使用memento来保存user对象的当前状态 9 memento.saveState("user",user); 10 11 //直接从memento中的storage对象读取值,证明user对象的状态已使用JSON格式保存 12 console.log(memento.storage["user"]);//{"name":"wing","age":70} 13 14 //现在来随便改动user对象的值 15 user.name="zhangwei"; 16 user.age=21; 17 18 //输出user对象的当前状态 19 console.log(JSON.stringify(user));//{"name":"zhangwei","age":21} 20 21 //当想要恢复user对象上一次所保存的状态时,只需要简单地调用memento的restoreSate()方法即可 22 user=memento.restoreState("user"); 23 24 //输出已被恢复至上一次所保存状态的user对象的新值 25 console.log(JSON.stringify(user));//{"name":"wing","age":70}
当需要在应用程序执行的特定时刻把其中的对象进行保存和恢复时,使用备忘录模式最为合适。
7.7 承诺模式
当处理异步函数时,通常我们会向异步函数传入回调函数。当异步函数完成了它的工作时,将会执行该回调函数来实现我们的某些操作。这会如我们所愿地执行,但唯一的问题是,这种做发法所产生的代码会变得难以阅读并且晦涩模糊。要知道,这个调用的函数是一个异步函数,传给它的函数用作了它的回调函数。如果想要等待若干异步函数运行完生成结果后再执行回调函数,会令所编写的代码愈发模糊,难以进行后续维护。现在向大家隆重介绍承诺(promiss)模式。此设计模式创建于20世纪70年代,后由CommonJs团队(http://wiki.commonjs.org/wiki/CommonJS)进行了改进,以应用于JavaScript。它定义了一种方式,实现了从一个异步函数的调用返回一个promise对象。此promise对象可以链式连接至对另一个函数的调用,该函数只有在此promise对象完成时才会执行,而此promise对象的完成要依赖于其对相应的异步函数执行的完成。这种做法有利于回调函数与异步函数调用的充分分离,可以改善代码的清晰程度,使得代码更具可读性,并因而更易于理解和维护。
promise对象在JavaScript中表现为一个包含着then()方法的对象实例。一旦处于执行中的异步函数完成执行,就会执行then()方法。我们来看一个简单的Ajax调用,它以一个回调函数作为其第2个参数,执行如下:
1 ajaxGet("/my-url", function(response) { 2 3 //对response进行相关处理 4 5 });
使用承诺模式,对相同的Ajax调用所用的JavaScript代码看起来将会如下:
1 ajaxGet("/my-url").then(function(response) { 2 3 //对response进行相关处理 4 5 });
你可能会认为这两者其实没有什么很大的不同。事实上后者更为清晰。它很明确地告诉我们,第2个函数将在第1个函数执行完毕后执行,然后在前者中只是隐晦地表达出了该意思。当处于存在多个异步调用的情况时,使用承诺模式就会显得比使用普通回调函数的相同代码更为清晰。如,我们来考虑以下代码,它首先向一个URL发出Ajax调用,然后再向另一个不同的URL发出第2个Ajax调用:
1 ajaxGet("/my-url", function() { 2 ajaxGet("/my-other-url", function() { 3 //进行相关处理 4 }); 5 });
如果使用承诺模式,此代码将会精简得更为易于理解,并可避免多层的代码嵌套。链中的异步调用越多,承诺模式的优点就越明显。
1 ajaxGet("/my-url").then(ajaxGet("my-other-url")).then(function(){ 2 3 //进行相关处理 4 5 });
在标准JavaScript中,当需要在多个同时发出的异步调用之后在执行某个单独的回调函数时,事情就会变得愈发复杂。而使用承诺模式,则需要简单地传入一个promises数组给all()方法,它就会同时执行数组中每项promises。当数组中的每一个方法都满足其各自对应promises时(所有项都成功完成之后),则返回一个单独的状态为fulfilled(成功满足)的promise,如下所示:
1 Promise.all([ajaxGet("/my-url"),ajaxGet("/my-other-url")]).then(function(){ 2 3 //对从这两个调用所返回的数据实施某些操作 4 5 });
JavaScript中的表示promises的“类”如代码清单7-12所示。
代码清单7-12 承诺模式
1 //定义代表promise的“类”,使得我们可以写出可读性高,易于理解的代码,来支持实现执行异步方法以及它们的回调函数。 2 //由此“类”创建的实例遵循Promises/A+规范(此规范的详细介绍请参见https://promisesaplus.com), 3 //并且能够通过所有的官方单元测试(https://github.com/promises-aplus/promises-tests,能通过测试便证明了其能够遵循该规范) 4 var Promise = (function() { 5 //定义promise对象可以拥有的3种可能状态:pending————默认值,“等待”,表示该promise尚未完成处理; 6 //fulfilled——“成功完成”,表示该promise已成功完成处理:rejected——“失败”,表示该promise处理失败并处理了错误 7 var state = { 8 PENDING: "pending", 9 FULFILLED: "fulfilled", 10 REJECTED: "rejected" 11 }; 12 //定义表示promise的“类”。如果在初始化时传入一个异步函数,则此异步函数将立即执行 13 function Promise(asyncFunction) { 14 var that = this; 15 //定义一个属性来表示该promise对象的当前状态,默认为pending 16 this.state = state.PENDING; 17 18 //定义一个属性,用于保存该异步方式执行完成时所要调用的各个回调函数(callback数组中存放的数组项时各个callback对象,callback对象包含一个promise对象,它可以含有fulfill方法和(或)reject方法)的清单 19 this.callbacks = []; 20 21 //定义一个属性,用于保存由该promise对象所表示的异步方法时所返回的值 22 this.value = null; 23 24 //定义一个属性,用于保存执行该异步方法时所产生的任何错误的详细信息 25 this.error = null; 26 27 //定义2个函数,它们将会传入由此promise表示的异步函数。如果该异步函数成功执行,则执行第1个函数; 28 //如果该异步函数不能成功执行,则执行第2个函数 29 function success(value) { 30 //执行测promise对象的resolve()方法,它会确保党此promise对象的异步函数成功执行时,与此 31 //promise对象相连的任何所要执行的函数(通常是then()中的回调函数)都能被执行 32 that.resolve(value); 33 } 34 35 function failure(reason) { 36 37 //执行此promise对象的resolve()方法,它将执行所有相连的回调函数,用于显示错误信息或对错误进行处理。 38 //与此promise对象按链式相连的所有更深层promise对象将不会执行 39 that.reject(reason); 40 } 41 //如果此promise对象在初始化时被传入一个异步函数,则此异步函数会立即执行,上面所定义的success()和failure()函数会作为参数传入。 42 //该异步函数必须根据其所尝试执行的行为的运行结果情况来确保这两个函数中最合适的那个得到执行 43 if(typeof asyncFunction === "function") { 44 asyncFunction(success, failure); 45 } 46 } 47 //定义then()方法。这是Promise/A+规范的关键。它实现了基于异步函数是否成功地完成其执行任务来使callback函数关联至该异步函数的运作结果。 48 //它使得多个promise可以在彼此之间实现链式连接。在当前的一个promise成功地完成之后,将执行下一个,从而实现了更进一步的异步函数的执行 49 Promise.prototype.then = function(onFulfilled, onRejected) { 50 //创建一个新的promise对象(在本方法的最后会返回此promise),以通过then()实现链式调用 51 var promise = new Promise(), 52 //定义一个callback对象,将其保存在promise对象中,并把新的promise实例关联其上,作为任何callback对象方法的上下文 53 //(可以把callback对象理解为此promise完成其异步函数的执行后所要执行的回调函数,回调函数可保存在callback对象的fulfill()和(或)reject()方法中). 54 callback = { 55 promise: promise 56 }; 57 //如果提供了一个函数,它在该异步函数成功地完成时执行,则保存咋这个函数callback对象中, 58 //callback对象还有那个心创建的promise对象作为这个函数的运行上下文(此时,该callback对象里有一个promise属性(promise属性的值是一个promise对象),callback对象里有一个fulfill方法) 59 if(typeof onFulfilled === "function") { 60 callback.fulfill = onFulfilled; 61 } 62 63 //如果提供了一个函数,它在该异步函数不能成功地完成时执行,则保存咋这个函数callback对象中, 64 //callback对象还有那个心创建的promise对象作为这个函数的运行上下文(此时,该callback对象里有一个promise属性(promise属性的值是一个promise对象),callback对象里有一个reject方法) 65 if(typeof onRejected === "function") { 66 callback.reject = onRejected; 67 } 68 //把该callback对象添加至callbacks数组 69 this.callbacks.push(callback); 70 71 //尝试执行callbacks数组所保存的各个callback对象。其异步函数已经完成执行后才进行此操作(执行callbacks数组中的callback对象所代表的回调函数)。 72 //如果异步函数没有执行完成。则当该“类”的其他代码调用executeCallback()时,才执行executeCallbacsk(),来调用callbacks对象中的回调函数 73 this.executeCallbacks(); 74 75 //返回该新创建的promise对象,使得可以通过重复调用then()方法来实现链式链接其他个异步函数 76 return promise; 77 }; 78 //定义一个方法,用于在此promise对象所对应的异步函数的执行已经完成时,执行此promise相关的所有callback对象 79 Promise.prototype.executeCallbacks = function() { 80 var that = this, 81 value, 82 callback; 83 //定义2个函数,如果此promise的callbacks数组中所保存的callback对象尚未保存相等同的fulfill()和reject()方法,则默认使用这2个函数 84 function fulfill(value) { 85 return value; 86 } 87 88 function reject(reason) { 89 throw reason; 90 } 91 //只有在该promise不处于未完成状态(即其对应的异步函数尚未完成执行)时,才执行callbacks数组中的各个callback对象 92 if(this.state !== state.PENDING) { 93 //Promises/A+规范的2.2.4节指出,promise对象的回调函数应当异步执行(promise对象的回调函数对应其callbacks数组的callback对象的fulfill()和(或)reject()方法), 94 //而脱离出所有的对then()的可能会发生的调用的这个流程。这样就确保了整个由promise组成的链准备就绪后,对回调函数的调用才发生。 95 //使用Oms的setTimeout可以给JavaScript引擎一个很短的时间来完成对整个promise链的处理,然后才是任何回调函数的运行。对于setTimeout的调用, 96 //各种浏览器都会有一个最小延时可能值(4ms或16ms)。因此,在实际运行中,promise对象的回调函数通常会在4ms之后才执行 97 setTimeout(function() { 98 //循环遍历此promise对象的callbacks数组中所有的callback对象,并依次执行每一个callback对象。 99 //如果该promise能够成功完成(通过其异步函数成功地完成了执行),则选择使用callback对象的fulfill方法;如果其异步函数在执行期间返回错误, 100 //则选择reject方法 101 while(that.callbacks.length) { 102 callback = that.callbacks.shift(); 103 104 //把callback对象的执行抱够在try/catch块中,以防它抛出错误。如果出现了错误, 105 //我们并不希望promise对象链停止执行,我们希望对该promise对象进行失败处理(使用reject方法), 106 //使得发起调用的代码可以自己来处理该错误 107 try { 108 //基于该promise对象的状态,执行响应的callback对象方法。如果callback对象 109 //并没被配置fulfill()和reject()方法,则退而使用默认的fulfill()和reject()方法。 110 //这两个方法在executeCallbacks()方法一开始的位置被定义,见上方 111 if(that.state === state.FULFILLED) { 112 value = (callback.fulfill || fulfill)(that.value); 113 } else { 114 value = (callback.reject || reject)(that.error); 115 } 116 //把该回调函数的执行结果传给resolve()方法,resolve()方法将会把该promise 117 //对象标记为“成功完成”或者继续进一步执行 118 callback.promise.resolve(value); 119 } catch(reason) { 120 //如果回调函数执行过程中出现了错误 121 callback.promise.reject(reason); 122 } 123 124 } 125 126 }, 0); 127 } 128 }; 129 130 //如果此promise对象在之前尚未处于“成功完成”或“失败”状态,则fulfill()方法将会把此promise 131 //对象标记为“完成”。在此promise对象相关联的所有回调函数将会在此时执行 132 Promise.prototype.fulfill = function(value) { 133 //如果此promise对象仍然处于“未完成”状态,才可以转换此promise对象至“完成”状态。 134 //此fulfill方法执行时会被传入一个值(在我们使用fulfill方法时根据需要而定,通常为此promise对象对应的异步函数的执行返回值) 135 if(this.state === state.PENDING && arguments.length) { 136 this.state = state.FULFILLED; 137 this.value = value; 138 this.executeCallbacks(); 139 } 140 }; 141 142 //如果此promise对象在之前尚未处于“成功完成”或“失败”状态,则reject()方法将会把promise对象标记为“失败”。 143 //与此promise对象相关联的所有回调函数将会在此时执行 144 Promise.prototype.reject = function(reason) { 145 146 //如果此promise仍然处于“未完成”状态,才可以转换此promise至“失败”状态。此reject方法执行时会被传入一个值 147 //(在我们使用reject方法时根据需要而定,通常为出错原因) 148 if(this.state === state.PENDING && arguments.length) { 149 this.state = state.REJECTED; 150 this.error = reason; 151 this.executeCallbacks(); 152 } 153 }; 154 155 //resolve()方法接收一个对promise对象的fulfill()回调函数的成功调用的返回值。如果该promise对象是 156 //位于这条有then()方法调用所形成的链的最后一个promise对象,则使用此返回值来使该promise对象完成。 157 //如果该promise对象不是链中的最后一个,就会继续往下处理该链,递归地对链式连接的promise对象进行 158 //相应的完成处理或失败处理 159 Promise.prototype.resolve = function(value) { 160 var promise = this, 161 162 //检测从callback对象的fulfill()方法所返回的值的类型。如果这是链的最后一个promise对象, 163 //该值就会是其异步函数本身的执行结果。如果此promise对象具有链式相连的其他promise对象, 164 //那么传给此方法的值将包含着另一个promise对象。递归地,它又会再次调用resolve()方法 165 valueIsThisPromise = promise === value, 166 valueIsApromise = value && value.constructor === Promise, 167 168 //词语"thenable" 是指这样的一个对象,它看起来像是一个promise对象,因为它包含有一个 169 //属于自己的then()方法,然而它不是这个Promise"类"的一个实例。当需要将其他实现了 170 //Promises/A+规范的类所产生的promise对象连接在一起时,这是很有帮助的 171 valueIsThenable = value && (typeof value === "object" || typeof value === "function"), 172 173 isExecuted = false, 174 then; 175 176 //如果传给此方法的值与这里所表示的promise对象相同,则把此promise进行失败处理。 177 //不然,我们就可能会陷入死循环 178 if(valueIsThisPromise) { 179 //Promises/A+规范指出,如果此promise对象与传给此方法的promise对象相同,则应该把一个TypeError传给reject()方法,从而有效地停止执行链中的更下一步的promise 180 promise.reject(new TypeError()); 181 182 //如果传给resolve()方法的值是这个Promise“类”的另一个案例,则基于所提供的promise对象的 183 //状态来把当前promise对象进行fulfill(成功完成)或reject(失败)处理 184 } else if(valueIsApromise) { 185 //如果传给resolve()方法的promise对象已经被进行过成功完成处理或失败处理,则把所传入的 186 //promise对象中包含的值或错误信息继续传给当前promise对象 187 if(value.state === state.FULFILLED) { 188 promise.fulfill(value.value); 189 } else if(value.state === state.REJECTED) { 190 promise.reject(value.error); 191 //如果传给resolve()方法的promise对象尚未进行成功完成处理或失败处理,则执行所传进来的 192 //promise对象的then()方法,以确保当所传进来的promise对象的异步函数完成执行时,当前 193 //promise对象可以连同所传进来的promise对象一起被处理或进行失败处理 194 } else { 195 value.then(function(value) { 196 promise.resolve(value); 197 }, function(reason) { 198 promise.reject(reason); 199 }); 200 } 201 //如果传给resolve()方法的值不是这个Promise“类”的一个实例,而是一个包含有自己的then()方法 202 //的类似对象,则执行它的then()方法,基于这个传入的promise对象的状态把当前promise对象进行成功完成处理或失败处理。 203 //当要连接其他与此Promise类一样实现了相同的规范的类所创建的promise对象时,这是很有帮助的 204 205 } else if(valueIsThenable) { 206 //把执行包裹在try/catch块中,以防在其他类型的promise对象的实现过程中在底层代码抛出错误 207 try { 208 then = value.then; 209 210 //如果该value变量中所保存的对象包含then()方法,则执行该then(),以确保当这个 211 //promise对象(该value变量中所保存的对象)完成其工作后,能够基于其结果使当前promise 212 //对象进行成功完成处理或失败处理 213 if(typeof then === "function") { 214 then.call(value, function(successValue) { 215 if(!isExecuted) { 216 isExecuted = true; 217 promise.resolve(successValue); 218 } 219 }, function(reason) { 220 if(!isExecuted) { 221 isExecuted = true; 222 promise.reject(successValue); 223 } 224 }); 225 } else { 226 promise.fulfill(value); 227 } 228 } catch(reason) { 229 if(!isExecuted) { 230 isExecuted = true; 231 promise.reject(reason); 232 } 233 } 234 //如果传给resolve()方法的值不是一个promise对象,则使用该值来使当前promise对象完成。 235 //与该promise对象关联的所有回调函数将会被执行 236 } else { 237 promise.fulfill(value); 238 } 239 240 }; 241 //增加一个额外的方法Promise.all()。它并不属于Promise/A+规范的一部分,但属于ECMAScript 6 Promises 242 //规范的一部分。此规范具有Promise机制的优点,直接集成到了JavaScript语言中。(在ECMAScript6中,Promise是JavaScript的原生对象之一) 243 244 //此方法接收一个promise对象的数组。每一个数组项都表示一个异步函数,这些异步函数会同时执行。all()方法最后会返回一个单独的promise对象, 245 //使得当数组中所有提供的promise都成功完成后可以执行一个单独的then()方法。在成功完成后,所传递的值是一个数组。该数组包含着promise对象数组中每个promise对象的执行返回值。 246 //返回值数组项的次序与原promise对象数组的次序相同 247 Promise.all = function(promises) { 248 var index = 0, 249 promiseCount = promises.length; 250 251 //返回一个单独的promise对象来代表提供给all()方法的所有promise对象。 252 //当所提供的所有promise对象都成功完成时,此promise对象就会进行成功完成处理 253 return new Promise(function(fulfill, reject) { 254 var promise, 255 results = [], 256 resultCount = 0; 257 258 //当所提供的promise对象中的某一个完成时,执行一次onSuccess()函数,把该promise对象的 259 //产生结果添加至数组中,所在的数组位置序号与该promise对象在原来的数组中的位置序号相同 260 function onSuccess(result, index) { 261 result[index] = result; 262 resultCount++; 263 264 //如果已经收集齐了所有promise对象的产生结果,则把当前这个单独的promise对象(代码所有promise对象的这个promise对象)进行 265 //成功完成处理,并把由各个独立promise对象的完成结果所组成的数组传入 266 if(resultCount === promiseCount) { 267 fulfill(results); 268 } 269 270 } 271 272 //如果所提供的promise对象当中某一个是“失败”的,则把当前promise对象(代码所有promise对象的这个promise对象)进行失败处理 273 274 function onError(error) { 275 reject(error); 276 } 277 278 //处理一个给定的promise对象。如果它能完成,则执行onSuccess();如果不能,则执行onError() 279 function resolvePromise(index, promise) { 280 promise.then(function(value) { 281 onSuccess(value, index); 282 }, onError); 283 } 284 285 //循环遍历提供给all()方法的所有promise对象,依次对其进行处理 286 for(; index < promiseCount; index++) { 287 promise = promises[index]; 288 resolvePromise(index, promise); 289 } 290 }); 291 292 } 293 return Promise; 294 }());
看看代码清单7-13,当中展示了如果利用代码清单7-12的Promise“类”来在代码中创建和使用成功模式
代码清单7-13 使用承诺模式
1 //定义一个变量,用作在本代码后面的毫秒计算器 2 var millisecondCount = 0; 3 4 //定义一个方法,向给定的URL以GET的请求方式获取所返回的数据。它会返回一个promise对象。 5 //可以使用该对象的then()方法来连接回调函数至该对象 6 function ajaxGet(url) { 7 //返回一个新的promise对象,以实施Ajax请求的异步函数对其进行初始化。当promise执行此函数时, 8 //它会传入2个函数参数。当该异步请求成功时则执行第1个函数,当该异步请求的执行出现了错误时则执行第2个函数 9 return new Promise(function(fulfill, reject) { 10 var xhr = new XMLHttpRequest(), 11 STATE_LOADED = 4, 12 STATUS_OK = 200; 13 xhr.onreadystatechange = function() { 14 if(xhr.readyState !== STATE_LOADED) { 15 return; 16 } 17 18 //如果该Ajax GET请求已成功地收到返回数据,则执行该fulfill方法 19 if(xhr.stats === STATUS_OK) { 20 fulfill(xhr.responseText); 21 //如果该Ajax GET请求不能成功地接收到返回数据,则执行该reject方法 22 } else { 23 reject("For the Url'" + url + "',the server responded with:" + xhr.status); 24 } 25 } 26 //实施Ajax GET请求 27 xhr.open("GET",url); 28 xhr.send(); 29 }); 30 } 31 32 //定义一个方法,确保经过给定的毫秒数后在进行计数。返回一个promise 33 function wait(milliseconds) { 34 return new Promise(function(fulfill, reject) { 35 36 //如果所提供的毫秒数的值是一个大于0的数值,则调用setTimeout方法来等待该毫秒数, 37 //然后执行fulfill方法 38 if(milliseconds && typeof milliseconds === "number" && milliseconds > 0) { 39 setTimeout(function() { 40 fulfill(milliseconds); 41 }, milliseconds); 42 } else { 43 reject("Not an acceptable value provided for millseconds:" + milliseconds); 44 } 45 }); 46 } 47 48 //定义2个函数,分别用于某特定的promise成功或失败之时 49 function onSuccess(milliseconds) { 50 alert(milliseconds + "ms passed"); 51 } 52 53 function onError(error) { 54 alert(error); 55 } 56 57 //例子1:成功 58 //使用一个我们确知能使之成功的值来执行wait()函数。这里将会执行所提供给then()方法的2个方法中的第1个 59 // wait(500).then(onSuccess, onError); //等待0.5s后,输出500ms passed 60 61 //例子2:错误 62 //使用一个我们确知不能使之成功的值来执行wait()函数。因为这会立即对promise对象进行失败处理,所以 63 //先会向用户演出这里的错误信息的提示框,然后才是例子1的结构提示框 64 // wait(0).then(onSuccess, onError); //Not an acceptable value provided for millseconds:0 65 66 //例子3:链式调用 67 //使用then()方法可以使多个promise链在一起,从而实现多个操作的按序执行。之前的一个异步函数的执行 68 //结果出来后,再执行下一个。如果不使用Promise模式,就需要使用复杂的多重嵌套回调函数来实现。 69 //Promise模式可以令其显著地简化 70 wait(1000).then(function(milliseconds) { 71 //在经过了1s的延迟后,用传入该函数参数的值(此例为1000)对毫秒计数器进行增加 72 millisecondCount += milliseconds; 73 74 //在这个函数里返回一个promise对象。这意味着,一旦之前的操作完成。 75 return wait(1600); 76 }).then(function(milliseconds) { 77 //到这一刻,已经过去了2600ms。该值保存在毫秒计数器变量中 78 millisecondCount += milliseconds; 79 80 //返回另一个promise对象,表示从现在起要进行400ms的延迟,然后再执行接下来的then()语句中所指定的函数 81 return wait(400); 82 }).then(function(milliseconds) { 83 //把刚过去的400ms增加至毫秒计数器变量中,现在它共为3000 84 millisecondCount += milliseconds; 85 86 //最后,输出经过这些步骤之后毫秒计数器的值,表示从此链的第一项操作开始到现在所过去的毫秒数 87 alert(millisecondCount + "ms passed"); //经过3秒之后 88 }); 89 90 //例子4:多个promise 91 //不同的promise对象可以链在一起。就如本例,先请求URL /page1.html(假设它存在于服务器),然后等待3s, 92 //再请求另一个URL /page2.html(同样,假设其存在) 93 ajaxGet("/page1.html").then(function() { 94 return wait(3000); 95 }).then(function() { 96 return ajaxGet("/page2.html"); 97 }).then(function() { 98 //只有当/page1.html和/page2.html都存在并可以访问时,此提示框才会出现 99 alert("/page1.html and /page2.html received,with a 3s gap between requests"); 100 }); 101 102 //例子5:多个promise同时进行 103 //Promise.all()方法接收一个以promise对象作为数组项的数组。这些promise对象将被同时进行处理, 104 //所得的结果会以数组的形式传给Promise的then()方法中的成功函数(then()方法中的第1个函数参数)。同时对/page1.html和/page2.html 105 //进行请求,当二者都完成时,执行then()当中的成功回调函数。两个文件(/page1.html和/page2.html) 106 //的内容会以数组的形式传给此成功函数作为参数,数组项的次序与promise对象数组的次序相同。如果所 107 //提供的promise对象当中任何一个出现失败情况,则执行的将会是那个错误回调函数。传入错误回调函数的 108 //参数是传给all()的参数中所出现的第1个错误的详细内容 109 Promise.all([ajaxGet("/page1.html"), ajaxGet("/page2.html")]).then(function() { 110 alert("/page1.html=" + files[0].length + " bytes. /page2.html = " + files[1].length + " bytes."); 111 }, function(error) { 112 alert(error); 113 });
当代码中存在很多异步操作,使得出现大量嵌套回调函数是,使用承诺模式最为合适。承诺模式使回调函数得以链式连接至异步调用,使得代码更易于阅读和理解。
7.8 策略模式
策略(strategy)模式的最适用场合是,某个“类”包含着大量的条件性语句(if-else或switch),每一条件分支都会引起该“类”的特定行为以不同方式作出改变。与其维护一段庞大的条件性语句,不如将每一行为划分成多个独立对象,每个对象被称为一个策略(strategy)。在每一次的应用中,这些策略对象中只有一个会应用至原来的对象,原来对象被称作客户(client)。设置多个策略对象还助于改进代码的质量,因为我们可以彼此独立地对这些策略对象进行单元测试。
代码清单7-14展示了一个适合应用策略模式的“类”的样例。当中包含了许多条件性语句,这些语句改变了从该“类”创建的各个对象的某项特定行为。
代码清单7-14 适用应用策略模式的代码
1 //定义一个“类”,表示HTML页面中的一个表单域 2 function FormField(type, displayText) { 3 this.type = type; 4 this.displayText = displayText; 5 6 //创建一个新的<input>标签,使用该“类”在实例化时所提供的type的值来设置此表单域的type标签特性 7 this.element = document.createElement("input"); 8 this.element = document.setAttribute("type", this.type); 9 10 //创建一个新的<labal>标签,使用该“类”在实例化时所提供的displayText的值来设置它显示的文本 11 this.label = document.createElement("label"); 12 this.label.innerHTML = this.displayText; 13 14 //添加该<labal>和<input>标签当前页面 15 document.body.appendChild(this.label); 16 document.body.appendChild(this.element); 17 } 18 19 //为每个表单域对象实例配备3个方法 20 FormField.prototype = { 21 22 //返回该表单域当前所保存的值 23 getValue: function() { 24 return this.element.value; 25 }, 26 27 //为表单域设置一个新值 28 setValue: function(value) { 29 this.element.value = value; 30 }, 31 32 //根据该表单域中的值是否有效来返回true或false 33 isValid: function() { 34 var isValid = false, 35 value; 36 37 //如果这是<input type="text">表单域,当它的值不是空字符串时,则以为它是有效的 38 if(this.type === "text") { 39 isValid = this.getValue() !== ""; 40 41 //如果这是<input type="email">表单域,若它的值不是空字符串,并且包含有@字符, 42 //在@之后还包含有.字符串,则认为它是有效的 43 } else if(this.type === "email") { 44 value = this.getValue(); 45 isValid = value !== "" && value.indexOf("@") > 0 && value.indexOf(".", value.indexOf("@")) > 0; 46 47 //如果这是<input type="number">表单域,如果它的值是数值,则认为它时有效的 48 } else if(this.type === "number") { 49 value = this.getValue(); 50 isValid = !isNaN(parseInt(value, 10)); 51 52 } else { 53 //其他 54 } 55 return isValid; 56 } 57 };
代码清单7-15展示了如果应用策略模式来对代码清单7-14中的代码进行重构,使之成为更高效、更易于管理的结构。
代码清单7-15 策略模式
1 //定义一个类“类”,表示HTML页面中的一个表单域。请注意,其在实例化时会传入一个新对象作为第3个参数。 2 //此对象包含我们所创建的特定类型的表单域的isValid()方法的特定实现。例如,text表单域所需要的isValid() 3 //方法是检查所保存的值是否为一个空字符串,因此我们创建一个包含着此方法的对象并在实例化时通过策略对象将其传入 4 function FormField(type, displayText, strategy) { 5 this.type = type || "text"; 6 this.displayText = displayText || ""; 7 8 this.element = document.createElement("input"); 9 this.element.setAttribute("type", this.type); 10 11 this.label = document.createElement("label"); 12 this.label.innerHTML = this.displayText; 13 14 //检查是否有包含isValid()方法的策略对象传入。如果有,则保存该策略对象,以便在此对象的isValid() 15 //方法执行时使用。如果没有提供策略对象,则使用默认对象 16 if(strategy && typeof strategy.isValid === "function") { 17 this.strategy = strategy; 18 } else { 19 this.strategy = { 20 isValid: function() { 21 return false; 22 } 23 } 24 } 25 document.body.appendChild(this.label); 26 document.body.appendChild(this.element); 27 } 28 29 FormField.prototype = { 30 getValue: function() { 31 return this.element.value; 32 }, 33 setValue: function(value) { 34 this.element.value = value; 35 }, 36 37 //通过调用所保存的策略对象中提供的isValid()方法来代替之前的isValid()的方法。不再需要使用多个 38 //if..else语句,从而使得此“类”的代码更加精简并更易于维护 39 isValid: function() { 40 return this.strategy.isValid.call(this); 41 } 42 }; 43 44 //定义3个策略对象,当FormField“类”实例化时,供3中不同类型的表单域使用。在这里,我们提供了isValid() 45 //方法的各种特定情况的实现,但我们可以继续扩展以下内容以包含更多的方法和(或)属性来满足我们的需求 46 //对于本例这种情况,我们可以创建出策略“类”,然后创建出若干对象作为该“类”的实例。在这里我们就有 47 //若干简单对象,这样就能使代码更灵活地保持精简并能够切中要点 48 var textFieldStrategy = { 49 50 //关于<input type="text">表单域验证的特定功能 51 isValid: function() { 52 return this.getValue() !== ""; 53 } 54 }, 55 emailFieldStrategy = { 56 57 //关于<input type="email">表单域验证的特定功能 58 isValid: function() { 59 var value = this.getValue(); 60 return value !== "" && value.indexOf("@") > 0 && value.indexOf(".", value.indexOf("@")) > 0; 61 } 62 }, 63 numberFieldStrategy = { 64 65 //关于<input type="number">表单域验证的特定功能 66 isValid: function() { 67 var value = this.getValue(); 68 return !isNaN(parseInt(value, 10)); 69 } 70 };
代码清单7-15中的代码可以如代码清单7-16所示般进行使用
代码清单7-16 使用策略模式
1 //为HTML页面创建3个表单域,每一个都有不同的类型。我们传入表单域类型,其关联的<label>标签的文本以及 2 //为此表单域类型提供表单域值验证所需行为的相关策略对象 3 var textField = new FormField("text", "First Name", textFieldStrategy), 4 emailField = new FormField("email", "Email", emailFieldStrategy), 5 numberField = new FormField("number", "Age", numberFieldStrategy); 6 7 //为每个表单域设置我们确知能够通过验证的值 8 textField.setValue("wing"); 9 emailField.setValue("wingzw@qq.com"); 10 numberField.setValue(35); 11 12 //检查表单域中的值是否正确通过验证 13 alert(textField.isValid());//true 14 alert(emailField.isValid());//true 15 alert(numberField.isValid());//true 16 17 //使用我们确定不能通过验证的值来改变这些表单域的值 18 textField.setValue(""); 19 emailField.setValue("zhangwei"); 20 numberField.setValue("wing"); 21 22 //检查以确保isValid()方法能正确运行,反映相应新值验证的变化 23 alert(textField.isValid());//false 24 alert(emailField.isValid());//false 25 alert(numberField.isValid());//false
如果某个“类”中的方法的行为的实现需要使用到大量的条件性逻辑语句而导致代码难以管理,则使用策略模式最为合适。