DTree中致命的递归

    前不久我发表过一篇文章,名字叫《可爱又可恨的梅花雪》,其中介绍了meizz大侠赫赫有名的梅花雪树控件,而且对其中遇到的一些小问题进行了改造,后来有人评论说梅花雪树太复杂,不如DTree来得简便,这个确实如此!DTree的代码看起来要简便很多,所实现的功能也还很丰富(尽管还没有梅花雪树那么强大),在一般的菜单和导航控制中使用会很方便,而且不会有什么问题。我在改造梅花雪树之前,也曾尝试过DTree,并且也对其中的一些方法进行了改造,但后来不得不放弃,因为DTree中致命的递归导致了在大数据量构造树的节点时js报堆栈溢出的错误!

    先看看我对DTree的改造。

    1. DTree中图片的引用路径被写死在js代码里,这个完全可以写成一个变量,在需要时更改。

var imgPath = "img";
// Tree object
function dTree(objName) {
   
this.config = { target: null, folderLinks: true, useSelection: true, useCookies: true, useLines: true, useIcons: true, useStatusText: false, closeSameLevel: false, inOrder: false };
   
this.icon = { root: imgPath + "/base.gif", folder: imgPath + "/folder.gif", folderOpen: imgPath + "/folderopen.gif", node: imgPath + "/page.gif", empty: imgPath + "/empty.gif", line: imgPath + "/line.gif", join: imgPath + "/join.gif", joinBottom: imgPath + "/joinbottom.gif", plus: imgPath + "/plus.gif", plusBottom: imgPath + "/plusbottom.gif", minus: imgPath + "/minus.gif", minusBottom: imgPath + "/minusbottom.gif", nlPlus: imgPath + "/nolines_plus.gif", nlMinus: imgPath + "/tree/nolines_minus.gif" };
   
this.obj = objName;
   
this.aNodes = [];
   
this.aIndent = [];
   
this.root = new Node(-1);
   
this.selectedNode = null;
   
this.selectedFound = false;
   
this.completed = false;
}

    2. DTree中没有用于获取被选择节点的text值的方法,而其中的test方法完全可以被用来改造。

// Get all selected nodes text (e.g. a,b,c,d,e)
dTree.prototype.getText = function () {
   
var value = new Array();
   
for (var n = 0; n < this.aNodes.length; n++) {
       
if (this.aNodes[n].check === "true" || this.aNodes[n].check === true) {
            value[value.length]
= this.aNodes[n].name;
        }
    }
   
return value;
};

    3. DTree中没有考虑节点的CheckBox的disabled状态,由于节点中本来就包含CheckBox控件,所以可以直接使用它的disabled属性来进行控制。

var treeMode = "General"; //General or Special
//
Event of the node check
dTree.prototype.checkNode = function(nodeId) {
   
var check = document.getElementById(("c" + this.obj) + nodeId);
   
var node = this.getNode(nodeId);
    node.check
= check.checked;
   
if (check.checked) {
       
if (treeMode == "Special") {
           
this.enableChildreNode(node, true);
        }
       
else {
           
this.checkChildreNode(node, true);
        }
       
//this.checkParentNode(node, true);
    } else {
       
if (treeMode == "Special") {
           
this.enableChildreNode(node, false);
        }
       
else {
           
this.checkChildreNode(node, false);
        }
    }
};

// Check all child nodes of the current node (recursive)
dTree.prototype.checkChildreNode = function (node, check) {
   
for (var i = 0; i < node.cNode.length; i++) {
       
var pCheck = document.getElementById(("c" + this.obj) + node.cNode[i].id);
       
if (pCheck && !pCheck.disabled) {
            pCheck.checked
= check;
            node.cNode[i].check
= check;
        }
       
if (node.cNode[i].cNode.length > 0) {
           
this.checkChildreNode(node.cNode[i], check);
        }
    }
};

dTree.prototype.enableChildreNode
= function(node, check) {
   
for (var i = 0; i < node.cNode.length; i++) {
       
var pCheck = document.getElementById(("c" + this.obj) + node.cNode[i].id);
       
if (pCheck) {
            pCheck.disabled
= check;
            node.cNode[i].check
= pCheck.checked = (check && !pCheck.disabled);

        }
       
if (node.cNode[i].cNode.length > 0) {
           
this.enableChildreNode(node.cNode[i], check);
        }
    }
}

     说明一下,我增加了一个treeMode变量来进行判断,当值为General时,在树中check节点时其子节点均被check;当值为Special时,在树中check节点时其子节点均被check而且被disabled。enableChildreNode方法用来disabled所有的子节点并且被check。同时,checkChildreNode方法中需要判断子节点的disabled状态,被disabled的子节点其check状态不能被改变。

    4. 修改了DTree中node方法里Render节点时的事件注册代码。

旧的代码是:

str += "<input  name='" + this.obj + "' id='c" + this.obj + node.id + "'onclick='" + this.obj + ".checkNode(" + node.id + ")' type='checkbox' " + (node.check === "true" ? "checked='checked'" : "") + " />";
改造后的代码是:
str += "<input id='c" + this.obj + node.id + "' onclick='javascript: " + this.obj + ".checkNode(\"" + node.id + "\")' type='checkbox' " + (node.check === "true" ? "checked='checked'" : "") + " />";

    以上是我在使用DTree时根据需要对DTree的部分改造。下面说说我所遇到的问题。

    首先,DTree的构造直接采用了dTree.add()方法,也就是说需要在一个循环或递归中去add所有的节点,如果事先知道树的层级,我们用n次循环可以解决问题,但如果事先不知道树的层级,就需要使用递归,在js中如果递归过深会引发堆栈溢出的错误。

    其次,细查DTree中的代码,发现很多地方其实都是用递归实现的算法,这也就是为什么我尽管解决了在递归中add节点的问题后还是会不断地出现堆栈溢出的错误。来看看这些导致错误的致命的递归。

    1. checkChildreNode方法中的递归在check一个父节点的时候会触发,如果这个父节点下的分支和子节点特别多,即使不引发堆栈溢出的错误,速度估计也很慢。

dTree.prototype.checkChildreNode = function (node, check) {
    
for (var i = 0; i < node.cNode.length; i++) {
        
var pCheck = document.getElementById(("c" + this.obj) + node.cNode[i].id);
        
if (pCheck && !pCheck.disabled) {
            pCheck.checked 
= check;
            node.cNode[i].check 
= check;
        }
        
if (node.cNode[i].cNode.length > 0) {
            
this.checkChildreNode(node.cNode[i], check);
        }
    }
};
    2. 最致命的地方在这里,render树时会调用addNode方法,而addNode方法又会不断调用node方法,这两个方法之间形成了一个递归调用的过程,如果节点过多,会引发堆栈溢出的错误。这个递归同样会由dtree.tostring()方法引起,它调用addNode,然后addNode调用node,之后node又调用addNode...如此反复,最终形成了罪恶的深渊!
// Creates the node icon, url and text
dTree.prototype.node = function (node, nodeId) {
    
var str = "<div class=\"dTreeNode\">" + this.indent(node, nodeId);
    
if (this.config.useIcons) {
        
if (!node.icon) {
            node.icon 
= (this.root.id == node.pid) ? this.icon.root : ((node._hc) ? this.icon.folder : this.icon.node);
        }
        
if (!node.iconOpen) {
            node.iconOpen 
= (node._hc) ? this.icon.folderOpen : this.icon.node;
        }
        
if (this.root.id == node.pid) {
            node.icon 
= this.icon.root;
            node.iconOpen 
= this.icon.root;
        }
        
if (node.check && node.check != "") {
        
//  onclick='alert(nodeId)'
            str += "<input id='c" + this.obj + node.id + "' onclick='javascript: " + this.obj + ".checkNode(\"" + node.id + "\")' type='checkbox' " + (node.check === "true" ? "checked='checked'" : ""+ " />";
        } 
else {
            str 
+= "<img id=\"i" + this.obj + nodeId + "\" src=\"" + ((node._io) ? node.iconOpen : node.icon) + "\" alt=\"\" />";
        }
    }
    
if (node.url) {
        str 
+= "<a id=\"s" + this.obj + nodeId + "\" class=\"" + ((this.config.useSelection) ? ((node._is ? "nodeSel" : "node")) : "node") + "\" href=\"" + node.url + "\"";
        
if (node.title) {
            str 
+= " title=\"" + node.title + "\"";
        }
        
if (node.target) {
            str 
+= " target=\"" + node.target + "\"";
        }
        
if (this.config.useStatusText) {
            str 
+= " onmouseover=\"window.status='" + node.name + "';return true;\" onmouseout=\"window.status='';return true;\" ";
        }
        
if (this.config.useSelection && ((node._hc && this.config.folderLinks) || !node._hc)) {
            str 
+= " onclick=\"javascript: " + this.obj + ".s(" + nodeId + ");\"";
        }
        str 
+= ">";
    } 
else {
        
if ((!this.config.folderLinks || !node.url) && node._hc && node.pid != this.root.id) {
            str 
+= "<a href=\"javascript: " + this.obj + ".o(" + nodeId + ");\" class=\"node\">";
        }
    }
    str 
+= node.name;
    
if (node.url || ((!this.config.folderLinks || !node.url) && node._hc)) {
        str 
+= "</a>";
    }
    str 
+= "</div>";
    
if (node._hc) {
        str 
+= "<div id=\"d" + this.obj + nodeId + "\" class=\"clip\" style=\"display:" + ((this.root.id == node.pid || node._io) ? "block" : "none") + ";\">";
        str 
+= this.addNode(node);
        str 
+= "</div>";
    }
    
this.aIndent.pop();
    
return str;
};
    3. openTo()方法中的递归和closeAllChildren()方法中的递归。同样,closeAllChildren()方法中的递归会由closeLevel()方法引起。
dTree.prototype.openTo = function (nId, bSelect, bFirst) {
    
if (!bFirst) {
        
for (var n = 0; n < this.aNodes.length; n++) {
            
if (this.aNodes[n].id == nId) {
                nId 
= n;
                
break;
            }
        }
    }
    
var cn = this.aNodes[nId];
    
if (cn.pid == this.root.id || !cn._p) {
        
return;
    }
    cn._io 
= true;
    cn._is 
= bSelect;
    
if (this.completed && cn._hc) {
        
this.nodeStatus(true, cn._ai, cn._ls);
    }
    
if (this.completed && bSelect) {
        
this.s(cn._ai);
    } 
else {
        
if (bSelect) {
            
this._sn = cn._ai;
        }
    }
    
this.openTo(cn._p._ai, falsetrue);
};

 

dTree.prototype.closeAllChildren = function (node) {
    
for (var n = 0; n < this.aNodes.length; n++) {
        
if (this.aNodes[n].pid == node.id && this.aNodes[n]._hc) {
            
if (this.aNodes[n]._io) {
                
this.nodeStatus(false, n, this.aNodes[n]._ls);
            }
            
this.aNodes[n]._io = false;
            
this.closeAllChildren(this.aNodes[n]);
        }
    }
};

    另外,大部分的方法中都包含了循环,整个代码的结构就是循环套递归,递归套循环。当节点数少时是发现不了问题的,节点数多的时候还不仅仅是加载树的过程特别慢,直接就是一个致命的错误——堆栈溢出!事实上,我所用来测试的数据不超过1000个节点,问题的关键就在于DTree树不像梅花雪树那样去动态地加载节点,它是一次性将所有的节点都加载完毕。当然,如果树比较小,DTree在使用上还是很方便的,毕竟它的结构比梅花雪树要简单,也没有那些过多的资源和js脚本需要引入。

    最后,我在后面还是给出我测试用的代码吧,读者可以使用Tree.htm文件中的测试数据进行测试,我给了两组数据用来测试,一组数据节点比较少,是可以正常使用的,一组数据节点很多,会报堆栈溢出的错误。

代码下载

posted @ 2009-03-24 16:29  Jaxu  阅读(7160)  评论(6编辑  收藏  举报