JavaScript内核系列 第8章 面向对象的JavaScript(下)
原创作者: abruzzi
接上篇:JavaScript内核系列 第8章 面向对象的JavaScript(上)
8.4实例:事件分发器
这一节,我们通过学习一个面向对象的实例来对JavaScript的面向对象进行更深入的理解,这个例子不能太复杂,涉及到的内容也不能仅仅为继承,多态等概念,如果那样,会失去阅读的乐趣,最好是在实例中穿插一些讲解,则可以得到最好的效果。
本节要分析的实例为一个事件分发器(Event Dispatcher),本身来自于一个实际项目,但同时又比较小巧,我对其代码做了部分修改,去掉了一些业务相关的部分。
事件分发器通常是跟UI联系在一起的,UI中有多个组件,它们之间经常需要互相通信,当UI比较复杂,而页面元素的组织又不够清晰的时候,事件的处理会非常麻烦。在本节的例子中,事件分发器为一个对象,UI组件发出事件到事件分发器,也可以注册自己到分发器,当自己关心的事件到达时,进行响应。如果你熟悉设计模式的话,会很快想到观察者模式,例子中的事件分发器正式使用了此模式。
Js代码
- var uikit = uikit || {};
- uikit.event = uikit.event || {};
- uikit.event.EventTypes = {
- EVENT_NONE : 0,
- EVENT_INDEX_CHANGE : 1,
- EVENT_LIST_DATA_READY : 2,
- EVENT_GRID_DATA_READY : 3
- };
var uikit = uikit || {};uikit.event = uikit.event || {}; uikit.event.EventTypes = { EVENT_NONE : 0, EVENT_INDEX_CHANGE : 1, EVENT_LIST_DATA_READY : 2, EVENT_GRID_DATA_READY : 3};
定义一个名称空间uikit,并声明一个静态的常量:EventTypes,此变量定义了目前系统所支持的事件类型。
Js代码
- uikit.event.JSEvent = Base.extend({
- constructor : function(obj){
- this.type = obj.type || uikit.event.EventTypes.EVENT_NONE;
- this.object = obj.data || {};
- },
- getType : function(){
- return this.type;
- },
- getObject : function(){
- return this.object;
- }
- });
uikit.event.JSEvent = Base.extend({ constructor : function(obj){ this.type = obj.type || uikit.event.EventTypes.EVENT_NONE; this.object = obj.data || {}; }, getType : function(){ return this.type; }, getObject : function(){ return this.object; }});
定义事件类,事件包括类型和事件中包含的数据,通常为事件发生的点上的一些信息,比如点击一个表格的某个单元格,可能需要将该单元格所在的行号和列号包装进事件的数据。
Js代码
- uikit.event.JSEventListener = Base.extend({
- constructor : function(listener){
- this.sense = listener.sense;
- this.handle = listener.handle || function(event){};
- },
- getSense : function(){
- return this.sense;
- }
- });
uikit.event.JSEventListener = Base.extend({ constructor : function(listener){ this.sense = listener.sense; this.handle = listener.handle || function(event){}; }, getSense : function(){ return this.sense; }});
定义事件监听器类,事件监听器包含两个属性,及监听器所关心的事件类型sense和当该类型的事件发生后要做的动作handle。
Js代码
- uikit.event.JSEventDispatcher = function(){
- if(uikit.event.JSEventDispatcher.singlton){
- return uikit.event.JSEventDispatcher.singlton;
- }
- this.listeners = {};
- uikit.event.JSEventDispatcher.singlton = this;
- this.post = function(event){
- var handlers = this.listeners[event.getType()];
- for(var index in handlers){
- if(handlers[index].handle && typeof handlers[index].handle == "function")
- handlers[index].handle(event);
- }
- };
- this.addEventListener = function(listener){
- var item = listener.getSense();
- var listeners = this.listeners[item];
- if(listeners){
- this.listeners[item].push(listener);
- }else{
- var hList = new Array();
- hList.push(listener);
- this.listeners[item] = hList;
- }
- };
- }
- uikit.event.JSEventDispatcher.getInstance = function(){
- return new uikit.event.JSEventDispatcher();
- };
uikit.event.JSEventDispatcher = function(){ if(uikit.event.JSEventDispatcher.singlton){ return uikit.event.JSEventDispatcher.singlton; } this.listeners = {}; uikit.event.JSEventDispatcher.singlton = this; this.post = function(event){ var handlers = this.listeners[event.getType()]; for(var index in handlers){ if(handlers[index].handle && typeof handlers[index].handle == "function") handlers[index].handle(event); } }; this.addEventListener = function(listener){ var item = listener.getSense(); var listeners = this.listeners[item]; if(listeners){ this.listeners[item].push(listener); }else{ var hList = new Array(); hList.push(listener); this.listeners[item] = hList; } };} uikit.event.JSEventDispatcher.getInstance = function(){ return new uikit.event.JSEventDispatcher(); };
这里定义了一个单例的事件分发器,同一个系统中的任何组件都可以向此实例注册自己,或者发送事件到此实例。事件分发器事实上需要为何这样一个数据结构:
Js代码
- var listeners = {
- eventType.foo : [
- {sense : "eventType.foo", handle : function(){doSomething();}}
- {sense : "eventType.foo", handle : function(){doSomething();}}
- {sense : "eventType.foo", handle : function(){doSomething();}}
- ],
- eventType.bar : [
- {sense : "eventType.bar", handle : function(){doSomething();}}
- {sense : "eventType.bar", handle : function(){doSomething();}}
- {sense : "eventType.bar", handle : function(){doSomething();}}
- ],..
- };
var listeners = { eventType.foo : [ {sense : "eventType.foo", handle : function(){doSomething();}} {sense : "eventType.foo", handle : function(){doSomething();}} {sense : "eventType.foo", handle : function(){doSomething();}} ], eventType.bar : [ {sense : "eventType.bar", handle : function(){doSomething();}} {sense : "eventType.bar", handle : function(){doSomething();}} {sense : "eventType.bar", handle : function(){doSomething();}} ],..};
当事件发生之后,分发器会找到该事件处理器的数组,然后依次调用监听器的handle方法进行相应。好了,到此为止,我们已经有了事件分发器的基本框架了,下来,我们开始实现我们的组件(Component)。
组件要通信,则需要加入事件支持,因此可以抽取出一个类:
Js代码
- uikit.component = uikit.component || {};
- uikit.component.EventSupport = Base.extend({
- constructor : function(){
- },
- raiseEvent : function(eventdef){
- var e = new uikit.event.JSEvent(eventdef);
- uikit.event.JSEventDispatcher.getInstance().post(e);
- },
- addActionListener : function(listenerdef){
- var l = new uikit.event.JSEventListener(listenerdef);
- uikit.event.JSEventDispatcher.getInstance().addEventListener(l);
- }
- });
uikit.component = uikit.component || {}; uikit.component.EventSupport = Base.extend({ constructor : function(){ }, raiseEvent : function(eventdef){ var e = new uikit.event.JSEvent(eventdef); uikit.event.JSEventDispatcher.getInstance().post(e); }, addActionListener : function(listenerdef){ var l = new uikit.event.JSEventListener(listenerdef); uikit.event.JSEventDispatcher.getInstance().addEventListener(l); }});
继承了这个类的类具有事件支持的能力,可以raise事件,也可以注册监听器,这个EventSupport仅仅做了一个代理,将实际的工作代理到事件分发器上。
Js代码
- uikit.component.ComponentBase = uikit.component.EventSupport.extend({
- constructor: function(canvas) {
- this.canvas = canvas;
- },
- render : function(datamodel){}
- });
uikit.component.ComponentBase = uikit.component.EventSupport.extend({ constructor: function(canvas) { this.canvas = canvas; }, render : function(datamodel){}});
定义所有的组件的基类,一般而言,组件需要有一个画布(canvas)的属性,而且组件需要有展现自己的能力,因此需要实现render方法来画出自己来。
我们来看一个继承了ComponentBase的类JSList:
Js代码
- uikit.component.JSList = uikit.component.ComponentBase.extend({
- constructor : function(canvas, datamodel){
- this.base(canvas);
- this.render(datamodel);
- },
- render : function(datamodel){
- var jqo = $(this.canvas);
- var text = "";
- for(var p in datamodel.items){
- text += datamodel.items[p] + ";";
- }
- var item = $("<div></div>").addClass("component");
- item.text(text);
- item.click(function(){
- jqo.find("div.selected").removeClass("selected");
- $(this).addClass("selected");
- var idx = jqo.find("div").index($(".selected")[0]);
- var c = new uikit.component.ComponentBase(null);
- c.raiseEvent({
- type : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
- data : {index : idx}
- });
- });
- jqo.append(item);
- },
- update : function(event){
- var jqo = $(this.canvas);
- jqo.empty();
- var dm = event.getObject().items;
- for(var i = 0; i < dm.length();i++){
- var entity = dm.get(i).item;
- jqo.append(this.createItem({items : entity}));
- }
- },
- createItem : function(datamodel){
- var jqo = $(this.canvas);
- var text = datamodel.items;
- var item = $("<div></div>").addClass("component");
- item.text(text);
- item.click(function(){
- jqo.find("div.selected").removeClass("selected");
- $(this).addClass("selected");
- var idx = jqo.find("div").index($(".selected")[0]);
- var c = new uikit.component.ComponentBase(null);
- c.raiseEvent({
- type : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
- data : {index : idx}
- });
- });
- return item;
- },
- getSelectedItemIndex : function(){
- var jqo = $(this.canvas);
- var index = jqo.find("div").index($(".selected")[0]);
- return index;
- }
- });
uikit.component.JSList = uikit.component.ComponentBase.extend({ constructor : function(canvas, datamodel){ this.base(canvas); this.render(datamodel); }, render : function(datamodel){ var jqo = $(this.canvas); var text = ""; for(var p in datamodel.items){ text += datamodel.items[p] + ";"; } var item = $("<div></div>").addClass("component"); item.text(text); item.click(function(){ jqo.find("div.selected").removeClass("selected"); $(this).addClass("selected"); var idx = jqo.find("div").index($(".selected")[0]); var c = new uikit.component.ComponentBase(null); c.raiseEvent({ type : uikit.event.EventTypes.EVENT_INDEX_CHANGE, data : {index : idx} }); }); jqo.append(item); }, update : function(event){ var jqo = $(this.canvas); jqo.empty(); var dm = event.getObject().items; for(var i = 0; i < dm.length();i++){ var entity = dm.get(i).item; jqo.append(this.createItem({items : entity})); } }, createItem : function(datamodel){ var jqo = $(this.canvas); var text = datamodel.items; var item = $("<div></div>").addClass("component"); item.text(text); item.click(function(){ jqo.find("div.selected").removeClass("selected"); $(this).addClass("selected"); var idx = jqo.find("div").index($(".selected")[0]); var c = new uikit.component.ComponentBase(null); c.raiseEvent({ type : uikit.event.EventTypes.EVENT_INDEX_CHANGE, data : {index : idx} }); }); return item; }, getSelectedItemIndex : function(){ var jqo = $(this.canvas); var index = jqo.find("div").index($(".selected")[0]); return index; }});
首先,我们的画布其实是一个共jQuery选择的选择器,选择到这个画布之后,通过jQuery则可以比较容易的在画布上绘制组件。
在我们的实现中,数据与视图是分离的,我们通过定义这样的数据结构:
Js代码
- {items : ["China", "Canada", "U.S.A", "U.K", "Uruguay"]};
{items : ["China", "Canada", "U.S.A", "U.K", "Uruguay"]};
则可以render出如下图所示的List:
好,既然组件模型已经有了,事件分发器的框架也有了,相信你已经迫不及待的想要看看这些代码可以干点什么了吧,再耐心一下,我们还要写一点代码:
Js代码
- $(document).ready(function(){
- var ldmap = new uikit.component.ArrayLike(dataModel);
- ldmap.addActionListener({
- sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
- handle : function(event){
- var idx = event.getObject().index;
- uikit.component.EventGenerator.raiseEvent({
- type : uikit.event.EventTypes.EVENT_GRID_DATA_READY,
- data : {rows : ldmap.get(idx).grid}
- });
- }
- });
- var list = new uikit.component.JSList("div#componentList", []);
- var grid = new uikit.component.JSGrid("div#conditionsTable table tbody");
- list.addActionListener({
- sense : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
- handle : function(event){
- list.update(event);
- }
- });
- grid.addActionListener({
- sense : uikit.event.EventTypes.EVENT_GRID_DATA_READY,
- handle : function(event){
- grid.update(event);
- }
- });
- uikit.component.EventGenerator.raiseEvent({
- type : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
- data : {items : ldmap}
- });
- var colorPanel = new uikit.component.Panel("div#colorPanel");
- colorPanel.addActionListener({
- sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE,
- handle : function(event){
- var idx = parseInt(10*Math.random())
- colorPanel.update(idx);
- }
- });
- });
$(document).ready(function(){ var ldmap = new uikit.component.ArrayLike(dataModel); ldmap.addActionListener({ sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE, handle : function(event){ var idx = event.getObject().index; uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_GRID_DATA_READY, data : {rows : ldmap.get(idx).grid} }); } }); var list = new uikit.component.JSList("div#componentList", []); var grid = new uikit.component.JSGrid("div#conditionsTable table tbody"); list.addActionListener({ sense : uikit.event.EventTypes.EVENT_LIST_DATA_READY, handle : function(event){ list.update(event); } }); grid.addActionListener({ sense : uikit.event.EventTypes.EVENT_GRID_DATA_READY, handle : function(event){ grid.update(event); } }); uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_LIST_DATA_READY, data : {items : ldmap} }); var colorPanel = new uikit.component.Panel("div#colorPanel"); colorPanel.addActionListener({ sense : uikit.event.EventTypes.EVENT_INDEX_CHANGE, handle : function(event){ var idx = parseInt(10*Math.random()) colorPanel.update(idx); } });});
使用jQuery,我们在文档加载完毕之后,新建了两个对象List和Grid,通过点击List上的条目,如果这些条目在List的模型上索引发生变化,则会发出EVENT_INDEX_CHAGE事件,接收到这个事件的组件或者DataModel会做出相应的响应。在本例中,ldmap在接收到EVENT_INDEX_CHANGE事件后,会组织数据,并发出EVENT_GRID_DATA_READY事件,而Grid接收到这个事件后,根据事件对象上绑定的数据模型来更新自己的UI。
上例中的类继承关系如下图:
图 事件分发器类层次
应该注意的是,在绑定完监听器之后,我们手动的触发了EVENT_LIST_DATA_READY事件,来通知List可以绘制自身了:
Js代码
- uikit.component.EventGenerator.raiseEvent({
- type : uikit.event.EventTypes.EVENT_LIST_DATA_READY,
- data : {items : ldmap}
- });
uikit.component.EventGenerator.raiseEvent({ type : uikit.event.EventTypes.EVENT_LIST_DATA_READY, data : {items : ldmap} });
在实际的应用中,这个事件可能是用户在页面上点击一个按钮,或者一个Ajax请求的返回,等等,一旦事件监听器注册完毕,程序就已经就绪,等待异步事件并响应。
点击List中的元素China,Grid中的数据发生变化
点击Canada,Grid中的数据同样发生相应的变化:
由于List和Grid的数据是关联在一起的,他们的数据结构具有下列的结构:
Js代码
- var dataModel = [{
- item: "China",
- grid: [
- [{
- dname: "Beijing",
- type: "string"
- },
- {
- dname: "ProductA",
- type: "string"
- },
- {
- dname: 1000,
- type: "number"
- }],
- [{
- dname: "ShangHai",
- type: "string"
- },
- {
- dname: "ProductB",
- type: "string"
- },
- {
- dname: 23451,
- type: "number"
- }],
- [{
- dname: "GuangZhou",
- type: "string"
- },
- {
- dname: "ProductB",
- type: "string"
- },
- {
- dname: 87652,
- type: "number"
- }]
- ]
- },...
- ];
var dataModel = [{ item: "China", grid: [ [{ dname: "Beijing", type: "string" }, { dname: "ProductA", type: "string" }, { dname: 1000, type: "number" }], [{ dname: "ShangHai", type: "string" }, { dname: "ProductB", type: "string" }, { dname: 23451, type: "number" }], [{ dname: "GuangZhou", type: "string" }, { dname: "ProductB", type: "string" }, { dname: 87652, type: "number" }] ]},...];
一个组件可以发出多种事件,同时也可以监听多种事件,所以我们可以为List的下标改变事件注册另一个监听器,监听器为一个简单组件Panel,当接收到这个事件后,该Panel会根据一个随机的颜色来重置自身的背景色(注意在List和Grid下面的灰色Panel):