论坛首页 Web前端技术论坛

遵循MVC模式,效仿JTree思想,用javascript实现树形结构教程

浏览 9050 次
精华帖 (2) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-07-12  
1.引言
  正如大家所看到的,这又是一位编程者再发明轮子了,我猜想我在写这篇文章的时候也许还有千千万万个重复发明轮子的程序员正在埋头思考着类似于“如何实现关于树形结构的展示”的问题。
  关于众多的实现树状结构的解决方案多不胜数,但我觉得这并不能阻挡我们实现自己的树的脚步。我写下来以便自己能回顾整理开发思路并分享我的经验。
2.一个失败的作品--实现一个一层树的解决方案
  这是一个最普遍的解决方案,思想是:直接在网页中写你所需要的div作为树的节点,然后写一个javascript程序来控制树的位置和显示。

 <!-- 树状结构的节点,正如看到的那样,这样的节点是直接写在网页上的-->
<body>
<div id="root" style="position:relative"><img src="root.gif"/><a href="#">射雕英雄传</a><br>
	<div id="0" style="position:relative;left:0px"><img src="childnoslibminus.gif" onclick="hide(0,this)"/><img src="folderopen.gif"/><a href="#">目录</a>
	<div style="position:relative;left:20px"><img src="t.gif"/><img src="page.gif"/><a href="#">目录1</a></div>
	<div style="position:relative;left:20px"><img src="turnline.gif"/><img src="page.gif"/><a href="#">目录2</a></div>
</div>
</body>

  上面的代码中,一个div(节点)包括了 id号(供js来引用),图片(根目录,文件夹或文件),带有连接的文字说明,还有改div的位置信息。
  这个解决方案的关键在于:使用div的嵌套实现树状结构的父子关系,这样我们就可以的到某个特定节点的id号,然后利用js的方法得到内嵌于该div中的子div(节点),对他进行隐藏/显示的操作。
function hide(idnum,pic){
  var parent=document.getElementById(eval(idnum));//得到父节点id号
  var children=parent.getElementsByTagName("div");//遍历所有子节点div,隐藏他们
  for(var i=0;i<children.length;i++){
	children[i].style.display="none";
  }
  addplusnoslib(idnum,pic);//更换节点把手,将展开的-号图片变成+号图片
}

  我们还需要完成上面那个更换图片把手的方法
function addplusnoslib(idnum,pic){
    pic.src="childnoslibplus.gif";//更换节点把手,将展开的-号图片变成+号图片
    pic.onclick=function (){//点击+号图片展开子节点的事件
	pic.src="childnoslibminus.gif";//将图片变成-号
	var parent=document.getElementById(eval(idnum));//遍历所有子结点,显示他们
	var children=parent.getElementsByTagName("div");
	for(var i=0;i<children.length;i++){
		children[i].style.visibility="true";
	}
	pic.onclick=function hide(){//把展开子节点事件覆盖掉,变成关闭子结点事件
		var parent=document.getElementById(eval(idnum));
		var children=parent.getElementsByTagName("div");
		for(var i=0;i<children.length;i++){
			children[i].style.display="none";
		}
		addplusnoslib(idnum,pic);
	};
     }
}


这样我们已经基本完成了对一个一层树状结构的构造了。
一条好消息和一条坏消息。好消息是:我们得到我们想要的轮子了。坏消息是:轮子是方的。

上面的方案经不起推敲
1)我们无法扩展这棵树。1个1层结构的树起不了什么作用,我们需要一个能满足目录层次更深的树。虽然我们可以再增加div的深度。但js可能无法实现复杂的循环。
2)我们不能碰他,因为这个代码非常脆弱。我们唯一可以碰的也许就是那几个带连接的名字


3.从失败走向成功

3.1从JTree得到的启发


在我们的失败的例子中已经展示了足够多的操作树的方法,而失败的主要原因是我们使用了一个方形的磨具来制造我们的轮子。因此我们需要有一个良好的磨具--MVC。我们可以看看JAVA中的JTree是如何实现树状结构的。
JTree主要用于在SWING中展示树,而模型的制作是交给DefaultTreeModel来完成的我们可以这样得到一棵JAVA的树。
DefaultMutableTreeNode root=new DefaultMutableTreeNode("China");
DefaultMutableTreeNode sh=new DefaultMutableTreeNode("Shanghai");
DefaultTreeModel model=new DefaultTreeModel(root);
JTree tree=new JTree(model);

JAVA已经给我们造好了房子,我们要做的是需要搬进去住就行了。而我们现在一无所有,需要从拌水泥开始。但是我们已经拥有了一张不错的设计图纸(MVC)和熟练的制造技术(javascript方法),那还有什么可担心的呢。


3.2付诸实施


在真正动手之前,必须清醒地认识到,我们不能让dom做我们的模型。我们必须把模型从dom中分离开来,让dom作为VIEW。(失败的例子中dom担负了太多的角色)。好了我们可以开始了。

3.2.1实现node


任何树都有节点。因此我们需要一个node类。

类的属性包括:id号(用来唯一确定node),nodeName(节点名字),parentNode(父节点),childNodes(子结点数组)
status(如果节点是文件夹,用来确定是打开还是关闭),visible(是可见还是被隐藏了)
我们的node类看上去像这样:

function node(name,parent){
	this.id=null;
	this.status=true;//文件夹是打开还是关闭状态
	this.name=name;
	this.parent=parent;//父节点(他也是node类型)
	this.visible=true;
	this.child=new Array();//子结点数组,存放的也是node类型
	this.addParent=function(parent){
		if(typeof(parent)=="object"){
			this.parent=parent;
		}
	}
	this.getParentName=function(){
		if(this.parent){
			return parent.name;
		}
	}
	this.addChild=function(value){
		if(typeof(value)=="object"){
			this.child.push(value);
		}
	}
	this.getChildNodes=function(){
		return this.child;
	}
	this.hasChild=function(){
		if(this.child.length>0)
			return true;
		else
			return false;
	}
//。。。(现在已经具备了基本的功能,今后还需要继续完善)
}


3.2.2实现treeModel

毫无疑问,一棵树的模型也是有很多树的节点组成的。我们在这个类中组装我们的node节点。


function treeMode(){
		var treeMode=this;
		var root=new node("Ajax in Action",null);//根元素
		var chapter1=new node("Chapter 1",root);//一级子类
		var chapter2=new node("Chapter 2",root);
		var para1=new node("para1",chapter1);//二级子类
		var para2=new node("para2",chapter1);
		var word1=new node("word1",para1);//三级子类
		var alpha1=new node("alpha1",word1);
		word1.addChild(alpha1);//添加子结点
		para1.addChild(word1);
		chapter1.addChild(para1);
		chapter2.addChild(para2);
		root.addChild(chapter1);
		root.addChild(chapter2);
   this.getTreeRoot=function(){
	return root;
   };
   return treeMode;
//。。。(现在已经具备了基本的功能,今后还需要继续完善)
}


3.2.3实现treeView

我们已经构建好了模型,接下来就是展示他。这是一个比较艰难的部分,但主要思想很简单:将node类型转换成相应的div,然后添加到body上就能呈现了。

function tree(){
	var body=document.body;
	var treemode=new treeMode();//这里的变量treemode如果改treeMode将会出错
	root=treemode.getTreeRoot();
	var tree=this;//将自己赋给tree变量,方便下面的函数调用
                           //同时防止了this转意出现错误


           //node->div每一个div有一个img描述(文件夹,文件,根目录)a标记表示节点名称
	this.getDiv=function(node){		
		var div=document.createElement("div");
		var img1=document.createElement("img");
		var a=document.createElement("a");
		a.href="#";
		var nodename=node.name;
		var txtnode=document.createTextNode(nodename);
		a.appendChild(txtnode);
		if(node.parent==null){//如果没有父节点,表明这个节点是根节点,我们把图片设成根节点对应的图片
			img1.src="root.gif";
		}
		else if(node.child.length>0){//如果有子结点,那么我们需要用一个文件夹的图片描述他
			var imghander=document.createElement("img");//每个文件夹前面都有一个把手,实现展开和关闭动作
			if(node.status){//根据文件夹状态挑选图片,这个是表示打开的文件夹
				img1.src="folderopen.gif";
	imghander.src="childnoslibminus.gif";
				div.appendChild(imghander);
				
			}
			else{
				img1.src="folderclose.gif";
	imghander.src="childnoslibplus.gif";
				div.appendChild(imghander);
				
			}
		}
		else{//其他情况就是文件节点(叶子节点)
			img1.src="page.gif";
			node.status=true;//其实叶子节点没有文件夹的打开和关闭状态,我们默认他为true
		}
		/*
		 * 根据node.visible设置div的可见性
		 * */
		if(!node.visible){
			div.style.display="none";
		}
		else{
			div.visibility="true";
		}
		div.appendChild(img1);
		div.appendChild(a);
		return div;
	};
	//递归方法,找到treemodel中的所有节点,将他们转换成div
	this.getTree=function(node){
		var div=this.getDiv(node);//调用上面的转换方法
		body.appendChild(div);
		if(node.hasChild()){
			var child=node.getChildNodes();
			for(var i=0;i<child.length;i++){
				tree.getTree(child[i]);
			}
		}
	}
	//调用一下,这样我们在调用tree()的时候就能得到树了
	this.getTree(root);
}


3.2.3查看结果
  我们已经完成了一棵树的构造,虽然是静态的,我们可以在body中调用tree()方法得到树
你可以在构造函数中去掉一个三级节点的时候,二级节点将会自动从一个文件夹变成文件的图标!

4.继续完善

4.1实现事件


接下来我们需要让这棵树动起来,我们需要为表示把手的img提供一个onclick事件。失败的模型中,我们直接改变div的style.display或visibility属性。而这个方案中我们只是让div作树的呈现,所以我们改变策略,让事件通知我们的tremodel模型。

4.1.1实现事件的关键:ID

要让treeview和treeModel联系起来的方法是:给他们分配同一个id。前面的代码中我们并没有初始化id。我们现在需要编写一个方法,实现node.id的自动编号。

var count=0;//全局变量
function addId(node){
	node.id=count;
	count++;
	if(node.hasChild()){
		var children=node.getChildNodes();
		for(var i=0;i<children.length;i++){
			addId(children[i]);//递归遍历所有的node节点
		}
	}
}


在treeMode()函数中调用addId(root);

function treeMode(){
      ....//其他省略
       addId(root);
}


在tree()函数内的getDiv()函数中为代表把手的图片添加和node.id相同的id号


function tree(){
     ....//其他省略
this.getDiv=function(node){
		var div=document.createElement("div");
		var img1=document.createElement("img");
		var a=document.createElement("a");
		a.href="#";
		var nodename=node.name;
		var txtnode=document.createTextNode(nodename);
		a.appendChild(txtnode);
		if(node.parent==null){
			img1.src="root.gif";
		}
		else if(node.child.length>0){
			var imghander=document.createElement("img");
			if(node.status){
				img1.src="folderopen.gif";
				imghander.src="childnoslibminus.gif";
				imghander.id=node.id;//这里添加我们的id
				imghander.onclick=function(){//添加一个事件,这样我们可以检验一下是否正确
				alert(this.id);
				}

				div.appendChild(imghander);
				
			}
			else{
				img1.src="folderclose.gif";
				imghander.src="childnoslibplus.gif";
				imghander.id=node.id;//这里添加我们的id
				imghander.onclick=function(){//添加一个事件,这样我们可以检验一下是否正确

				alert(this.id);
				}
	div.appendChild(imghander);
				
			}
		}
		else{
			img1.src="page.gif";
			node.status=true;
			//the page node has not closed or opened status
		}
		/*
		 * set div's visibility
		 * */
		if(!node.visible){
			div.style.display="none";
		}
		else{
			div.visibility="true";
		}
		div.appendChild(img1);
		div.appendChild(a);
		divlist.push(div);
		return div;
	};
}


测试一下这段代码,正确的弹出了id号

4.1.2根据id号找node

我们已经将div中的把手图片的id号和模型中node的id号对上了,只要根据这个id号就能找到node节点对象,然后就能轻易地改变node和他子结点的属性。我们需要在treeMode中添加一个findNodeById(node,id)方法

function treeMode(){
		...//其他属性
		this.clickedNode=null;//这是我们新添加的属性,用来保存findNode()的结果,方便其他函数使用。
		this.findNode=function(node,id){//findNodeById()方法
			var find=false;
			if(node.id==id&&!find){
				find=true;
				this.clickedNode=node;
			}
			else if(node.hasChild()&&!find){
				var child=node.getChildNodes();
				for(var i=0;i<child.length;i++){
					this.findNode(child[i],id);//递归遍历所有节点
				}
			}
		}
		...//其他方法
}


4.1.3实现展开/收缩方法

如果一个节点是的状态是关闭的,那么它所有的子孙节点都不可见。
但是一个节点如果是展开的,并不代表它所有的子孙节点都可见!试想一下如果1个2级文件夹试关闭的,那么打开一个1级文件夹后,2级文件夹内的文件还是不可见,因此,打开动作执行后文件的可见性只和文件夹的status(closed or opened)有关



可以看到这个方法应该作为节点的方法,所以我们在node类型中添加展开方法,而不是在树中。树的任务只需要调用一下node的setChildNodeClosed()。

function node(name,parent){
...//其他属性
	this.setChildNodeClosed=function(node){//收缩方法
		var node=node;
		if(node.hasChild()){
			var child=node.getChildNodes();
			for(var i=0;i<child.length;i++){
				if(child[i].visible){
					child[i].visible=false;
				}
				this.setChildNodeClosed(child[i]);
			}
		}
	}

...//其他方法
}
	this.setChildNodeOpened=function(node){//打开方法
		if(node.status==true){//如果文件夹的状态是打开的状态,遍历子结点,将可视性改成true,如果是关闭的,子结点不可见,我们无需动它,根据上面给出的红色提示想象一下
			var child=node.getChildNodes();
			for(var i=0;i<child.length;i++){
				if(!child[i].visible){
					child[i].visible=true;
					this.setChildNodeOpened(child[i]);//继续递归
				}
			}
		}
	}
}


为我们的treeMode增加一个setStatus()方法,方法内部是查找id号,然后改变一系列node的status

function treeMode(){
...//其他属性
		this.clickedNode=null;//刚才我们添加过的属性
		this.findNode=function(node,id){..//我们刚才实现过了}
this.changeStatus=function(id){//改变树中节点状态
			this.findNode(root,id);//找节点
			if(this.clickedNode){//节点找到了
				if(this.clickedNode.status){
					this.clickedNode.status=false;//如果文件是打开的,那关闭他
					this.clickedNode.setChildNodeClosed(this.clickedNode);
				}
				else{
					this.clickedNode.status=true;
					this.clickedNode.setChildNodeOpened(this.clickedNode);
				}
				
			}
		}
...//其他方法
}


最后在我们的onclick事件中添加这个changeStatus()方法

function tree(){
         ...//省略属性
	this.getDiv=function(node){
                  ...//省略		
		if(node.parent==null){
			img1.src="root.gif";
		}
		else if(node.child.length>0){
			var imghander=document.createElement("img");
			if(node.status){
				img1.src="folderopen.gif";
				imghander.src="childnoslibminus.gif";
				imghander.id=node.id;
				imghander.onclick=function(){
					var id=this.id;
					treemode.changeStatus(id);//调用tree模型的changeStatus()
					tree.getTree(root);
				}
				div.appendChild(imghander);
				
			}
			else{
				img1.src="folderclose.gif";
				imghander.src="childnoslibplus.gif";
				imghander.id=node.id;
				imghander.onclick=function(){
					var id=this.id;
					treemode.changeStatus(id);//调用tree模型的changeStatus()
					tree.getTree(root);//将我们改变状态后的模型显示出来
				}
				div.appendChild(imghander);
				
			}
		}
		else{
                        ...//省略			
		}
		...//省略设置可见性方法
	};
	
	this.getTree=function(node){
             ...//省略
	}
	this.getTree(root);
}


4.1.4检验代码

运行一下代码,点击chapter1的收缩图标后,浏览器中又增加了一棵树!我知道这并不是你想要的,但我们可以观察一下新加入的树,它的确正确的关闭了文件夹!我们最后需要作的仅仅是去掉老的那个树

4.1.5刷新树

要去掉已经在网页中显示的树,就要找到对应的div,从document.body中removeChild便可。但遍历整个页面并不可行,第一页面很大的话效率降低,第二容易造成误删。解决方法是我们可以给课div树做一个快照或索引,按照索操作不就行了吗。大白话就是--做一个存放div的数组。


function tree(){
...//其他省略
	var divlist=new Array();//存放div的数组
	this.getDiv=function(node){
		var div=document.createElement("div");
          	..//省略其他
          	          	imghander.onclick=function(){//我们的展开关闭事件
					var id=this.id;
					treemode.changeStatus(id);
					tree.refreshTree(root);//刷新树方法
				}

		divlist.push(div);//将所有新建的div都加入divlist中保管
		return div;
         }
         this.refreshTree=function(root){//包装一下3个方法
		this.removeTree();
		this.getTree(root);
		this.showTree();
	}
	this.removeTree=function(){//移除老树的方法
		for(var i=0;i<divlist.length;i++){
			document.body.removeChild(divlist[i]);
		}
		divlist=new Array();
	}
	this.getTree=function(node){//这是以前写过的方法,我们把其中的body.appendChild(div);去掉
		var div=this.getDiv(node);
		//body.appendChild(div);去掉他这个任务交给新的showTree()方法管理
		if(node.hasChild()){
			var child=node.getChildNodes();
			for(var i=0;i<child.length;i++){
				tree.getTree(child[i]);
			}
		}
	}
	this.showTree=function(){//显示新树的方法
		for(var i=0;i<divlist.length;i++){
			document.body.appendChild(divlist[i]);
		}
	}
}


5.测试并总结

测试你的代码,不出意外你将会的到一个比较完美的树形结构,有多完美?我们可以稍微考验一下我们的程序

function treeMode(){
		var treeMode=this;
		this.count=0;
		this.clickedNode=null;
		
		var root=new node("Ajax in Action",null);
		for(var i=0;i<100;i++){//这里就加个for循环吧
		var chapter1=new node("Chapter 1",root);
		var chapter2=new node("Chapter 2",root);
		var para1=new node("para1",chapter1);
		var para2=new node("para2",chapter1);
		var word1=new node("word1",para1);
		var alpha1=new node("alpha1",word1);
		word1.addChild(alpha1);
		para1.addChild(word1);
		chapter1.addChild(para1);
		chapter2.addChild(para2);
		root.addChild(chapter1);
		root.addChild(chapter2);
		}
		addId(root);
}


以上基本是按我如何一步步地实现这棵有点复杂的树的开发思路,JAVA 中JTree和DefaultTreeModel给与我很大的启发,使我完成了从最早的方形轮子到圆形轮子的进化过程,过程中不时遭遇到javascript语言制造的小麻烦和模型完善的困难。但完成之后也对MVC这种编程思想有了更进一步的认识。看完此文希望诸位对改变“重复发明”的偏见。
  • 大小: 3.5 KB
  • 大小: 11.8 KB
  • 大小: 7.3 KB
   发表时间:2008-07-12  

佩服LZ的精神.
不过你的代码和介绍写的太长,没有提纲,很难看明白.

类似你这样的问题,以前我在phpchina上也有过讨论,不过那是一个文本的php实现.

原理应该类似,都涉及到数组的Tree处理问题.

我在那个回复中有一个递归的算法,您有兴趣可以参考一下.

 

原始问题

递归技术问题

我的最终代码(php的,不过可以改成js)

第三页

 

0 请登录后投票
   发表时间:2008-07-12  
achun 写道

佩服LZ的精神.
不过你的代码和介绍写的太长,没有提纲,很难看明白.

类似你这样的问题,以前我在phpchina上也有过讨论,不过那是一个文本的php实现.

原理应该类似,都涉及到数组的Tree处理问题.

我在那个回复中有一个递归的算法,您有兴趣可以参考一下.

 

原始问题

递归技术问题

我的最终代码(php的,不过可以改成js)

第三页

 


感谢分享,希望能贴下js代码,php没有学过,呵呵
0 请登录后投票
   发表时间:2008-07-12  
terrynoya 写道
..............
感谢分享,希望能贴下js代码,php没有学过,呵呵

很高兴讨论,这个确实要实作一个的,我也需要用,不过因为不同应用的tree原始数据来源不同,可能方法就不会一样了.

这样,你先给我一个原始的数据样本,我来根据这个数据样本的结构写js.你原来帖子中的node函数是用来构建结构的,我要的样本是一个比如:

Array/Object的对象,里面的name和value表示了数据间的tree结构,

这样的原始数据样本.

0 请登录后投票
   发表时间:2009-01-14   最后修改:2009-01-14
lz的文章很有启发性。如果在把tree render加上就更丰富了,可以自定义每个节点的显示样式。

不知道能否提供你的所有代码,让我研究一下,谢谢!
0 请登录后投票
   发表时间:2009-02-02  
这是楼主的代码 我给test了一下 
0 请登录后投票
   发表时间:2009-02-02  
不错不错,思路很清晰
0 请登录后投票
   发表时间:2009-02-03  
我说两句啊……
(1)这样构建的树从数据库里拉几千条数据显示的速度怎么样?这种从数据库拉出数据渲染成树的方式显示在页面上要多少秒
(2)他能不能方便的实现节点的动态增删改以及位置变化(我不是说实现拖拉),只要能动态的移动即可(比如:把一个节点(下面可能还有好多字节点)挪到另外的节点下去),动态删除和挪动的时候如何处理内存泄漏问题
以上两点如能解决,就是非常成功的树
0 请登录后投票
   发表时间:2009-02-11  
楼主应该去看看jquery ui
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics