JavaScript组合模式在网上已经有很多人描述过了,我在《JavaScript设计模式之组合模式》中也写过但写的还不够完整,有些不知所云。我怕会误导大家,所以今天我想重新对“组合模式”做一下描述。如何可以更好的表述清楚《组合模式》的工作体系我想了很久但一直也不知道该怎么说才能说明白。最近用组合模式做了一棵树(后面会把代码贴出来供大家拍砖),组合模式说起来有些麻烦,我通过具体的操作感觉它就像是由多个单体组合而成。
这个要怎么理解呢?首先是“单体”(这是我自己的理解,呵呵),顾名思意就是组成结构的最小单元;由无数个这种单体组合而成一个“组合体”,但这个组合体又可以成为一个相对的单体再和其他的单体(组合体)继续组合成为一个新的组合体;如此循环下去直到目标的完成。这个单体与组合体可以有相同的方法和属性也可以有各自所特有的功能,通过这些方法和属性使单体与组合体可以更好的配合工作从而达到一个理想的状态。《变形金刚》我想大家都应该看过吧,里面会有很多的组合。可以组合成为一门火炮也可以组合成为别一个高大的机器人,这些都是由许多的单体通过相互的组合以及相互之间的衔接(接口)来完成一个巨大的工程;比如:有的机器人只能变形为一把枪(威震天),那这就只是一个独立的结点(叶结点);而其他部件可能会由一个或是几个机器人来协同组合而成(树结点),那这就是一个组合体(可能还会和其他的单体金刚再继续组合)。通过加入不同的单体来完成不一样的功能。
下面就来看一下我用组合模式写的树,其中用到了“观察者模式”。我使用“观察者模式”向树结点中的所有叶结点发布事件绑定信息和指定父结点操作信息。最初的想法是在加入(add()方法)结点的同时指定其父结点但这样做的后果就是:如果树结点中还有树结点那就造成了无限的嵌套,结果我的电脑死机了!改用观察者模式后就排除这种隐患,但使用观察者模式后有一个使用上的问题。就是说在页面加载完这棵树后,它一定是要只先加载所有树结点和叶结点,而不能同时展开树结点下的所有结点。因为如果要是都展开了就不能起到观察者的目的了,也就是说此树结点以内的其他叶结点(树结点)都不能够被绑定相关的事件(因为他们相关事件是要通过触发树结点来进行发布的)。世上的事儿,不管是什么事都要从两方面来考虑。虽然有之前的困惑但要是以后将这棵树改为动态加载的话(当点击树结点时展开并加载其所有叶结点)这种方法是很好的处理办法。不但可以加载其叶结点也可以对其进行相关事件的绑定, 一举两得。(初始化时,页面只加载第一级结点【树结点和叶结点】;然后再触发树结点的时候再去加载其第二级结点同时绑定相关事件)
罗嗦了半天,以下就是代码了。就下面的代码在开始之前我先做一个简单的介绍:
1、目前这棵树是在页面初始化后将所有的树结点和叶结点全部一次性的加入页面中,只是没有展开相关树结点中的叶结点而已。(准备做一下修改,将其改为动态加载的形式!)
2、树结点:当前结点中还有子结点的结点
3、叶结点:此结点处于结构中的最末端即叶子结点
在做如下程序的时候一定要做一个接口方法检查方法,这样可以判断被传入的对象是否已经具备了所需要的相应方法;如果没有则报错,否则继续。 (在这里为了少写点儿代码,我就省略了!但实际中不要省略!)
一、定义所的树结点都需要继承的类
// 树结点需要继承的属性和方法 function TreeMenuInterface(){ this.parent = null; // 存放当前结点的父结点。因为树结点也有可能会和其他叶结点再次组合成为一个新的树结点,所以它也需要有指向其父结点的功能。 }; TreeMenuInterface.prototype = { // 返回当前树结点 getElement : function(){ return this.treeNode; }, // 返回标题结点 getTitle : function(){ return this.itemTitle; }, // 返回树结点中所有的叶子结点 getLeafs : function(){ var items = []; for(var i = 0, l = this.menuItems.length; i < l; i++){ items[i] = this.menuItems[i].getElement(); } return items; }, // 设置当前叶结点的父结点 setLeafParent : function(parent){ this.parent = parent; return this; }, // 返回当前叶结点的父结点 getLeafParent : function(){ if(this.parent){ return this.parent; } }, // 向树结点中加入叶结点(组合的开始),可以传入叶结点也可以传入树结点 add : function(menuItem){ // 当前树结点中所拥有的叶结点 this.menuItems.push(menuItem); // 将叶子加入到树结点中 this.treeLeaf.appendChild(menuItem.getElement()); this.treeNode.appendChild(this.treeLeaf); return this; }, // 删除树结点中的叶结点 remove : function(){ // 删除操作,暂时未做 }, // 显示所有叶结点 show : function(){ this.treeLeaf.style.display = 'block'; return this; }, // 关闭所有叶结点 hide : function(){ this.treeLeaf.style.display = 'none'; return this; }, // 显示树结点中的所有叶子结点 getDisplay : function(el, style){ var display; if(!+'\v1'){ display = el.currentStyle[style]; }else{ display = document.defaultView.getComputedStyle(el, null).getPropertyValue(style); } return display; }, // 绑定树结点事件 bindEvent : function(type){ var _this = this; var leaf = this.getElement(); leaf['on'+type] = function(evt){ // 取消其冒泡事件 evt = evt || window.event; evt.stopPropagation ? evt.stopPropagation() : evt.cancelBubble = true; if(_this.parent){ alert('我的父结点是:'+_this.getLeafParent().getElement().getElementsByTagName('a')[0].innerHTML); } } }, // 叶结点的显示功能 bindIllustrate : function(){ var _this = this; var title = this.getElement(); // 点击当前树结点时显示其自身的所有叶结点并将叶结点指向自身 // 鼠标放上时展开所有叶结点 title.onmouseover = function(){ var leafs = _this.getElement().getElementsByTagName('ul')[0]; // 显示所有叶结点 (_this.getDisplay(leafs, 'display') == 'none') && _this.show(); // 利用“观察者模式”,将叶结点指向其自身的父结点并发布所有叶结点的绑定事件 for(var i = 0, l = _this.menuItems.length; i < l; i++){ _this.menuItems[i].setLeafParent(_this); _this.menuItems[i].bindEvent('click'); // 发布树结点中所的叶结点的提示信息为click时触发 } }; // 鼠标离开时关闭所有叶结点 title.onmouseout = function(){ var leafs = _this.getElement().getElementsByTagName('ul')[0]; // 隐藏所有叶结点 (_this.getDisplay(leafs, 'display') == 'block') && _this.hide(); } return this; } };
二、定义所有叶结点都需要继承的类
// 叶结点需要继承的属性和方法 function LeafInterface(){ this.parent = null; // 存放当前结点的父结点 }; LeafInterface.prototype = { // 返回当前结点 getElement : function(){ return this.item; }, // 设置当前叶结点的父结点。在下面的代码中被“观察者模式”利用此方法向叶结点中发布父结点信息 setLeafParent : function(parent){ this.parent = parent; }, // 返回当前叶结点的父结点 getLeafParent : function(){ return this.parent; }, // 绑定叶结点事件 bindEvent : function(type){ var _this = this; var leaf = this.getElement(); leaf['on'+type] = function(evt){ // 取消其冒泡事件 evt = evt || window.event; evt.stopPropagation ? evt.stopPropagation() : evt.cancelBubble = true; if(_this.parent){ alert('我的父结点是:'+_this.getLeafParent().getElement().getElementsByTagName('a')[0].innerHTML); } } } };
三、定义真正的树结点类(将会继承上面的树结点接口)
/** * 说明: * 1、树结点:当前结点中还有子结点的结点 * 2、叶结点:此结点处于结构中的最末端即叶子结点 */ // 树中具有叶结点的结点 function TreeMenu(title, href){ this.itemTitle = document.createElement('a'); // 树结点的标题 this.treeNode = document.createElement('li'); // 拥有叶结点的树结点 this.treeLeaf = document.createElement('ul'); // 树结点中存放叶子的区域 this.menuItems = []; // 存入树结点中的叶结点 this.treeNode.className = 'treeMenu'; this.itemTitle.innerHTML = title; this.itemTitle.href = href; // 绑定树结点的显示功能 this.bindIllustrate(); this.treeNode.appendChild(this.itemTitle); TreeMenuInterface.call(this); }; extend(TreeMenu, TreeMenuInterface);
四、定义真正的叶结点类(将会继承上面的叶结点接口)
// 树中的末端结点即叶结点 function TreeLeaf(title){ var _this = this; this.item = document.createElement('li'); // 叶子结点 this.parent = []; this.item.innerHTML = title; LeafInterface.call(this); }; extend(TreeLeaf, LeafInterface);
五、初始化(使用了一个简单工厂模式来完成加载)
// 整合在一个简单工厂中集中处理 var TreeFactory = { createTree : function(){ var tree = document.getElementById('tree'); var treeArea = document.createElement('ul'); var docFrag = document.createDocumentFragment(); // 创建一个文档碎片 // 以下均为叶结点 var leaf1 = new TreeLeaf('我是叶子结点1'); var leaf2 = new TreeLeaf('我是叶子结点2'); var leaf3 = new TreeLeaf('我是叶子结点3'); var leaf4 = new TreeLeaf('我是叶子结点4'); var leaf5 = new TreeLeaf('我是叶子结点5'); var leaf6 = new TreeLeaf('我是叶子结点6'); var leaf7 = new TreeLeaf('我是叶子结点7'); var leaf8 = new TreeLeaf('我是叶子结点8'); // 以下均为树结点,我有叶结点(leaf7和leaf8) var treeNode1_1 = new TreeMenu('我是树结点1-1', '#'); treeNode1_1.add(leaf7); treeNode1_1.add(leaf8); // 我是树结点且有叶结点(leaf3和leaf5),而且还有一个子树结点 var treeNode1 = new TreeMenu('我是树结点1', '#'); treeNode1.add(leaf3); // 树结点1下有叶结点3 treeNode1.add(leaf5); // 树结点1下有叶结点5 treeNode1.add(treeNode1_1); // 树结点1中还有一个树结点1-1 // 我是树结点且有叶结点(leaf4和leaf6) var treeNode2 = new TreeMenu('我是树结点2', '#'); treeNode2.add(leaf4); // 树结点2下有叶结点4 treeNode2.add(leaf6); // 树结点2下有叶结点6 // 存放是按照树型结构显示排列(最外层树结点) var nodes = this.add([leaf1, treeNode1, leaf2, treeNode2]); // 将所有结点临时存入文档碎片中之后一次性载入页面 for(var i = 0, l = nodes.length; i < l; i++){ docFrag.appendChild(nodes[i].getElement()); } treeArea.appendChild(docFrag); tree.appendChild(treeArea); }, // 获取树中的所有结点 add : function(leafs){ var items = []; for(var i = 0, l = leafs.length; i < l; i++){ items.push(leafs[i]); } return items; } };
这里面就是在加载结点的时候会比较多,以后可以用一个JSON来处理一下。这里就先不做了。下面是一个完整的例子: