在做tree封裝的時候,困難的地方往往是怎樣顯示樹形數(shù)據(jù),總后臺查詢的數(shù)據(jù)通常是List<Map<K,V>>之類的JSON字符串。形如
[{"NODELEVEL":2,"NODENAME":"測試深度節(jié)點20","FLAG":"1","HASCHILD":"1","PARENTNODE":"10002","LEVELCODE":"1000200000"}
,{"NODELEVEL":3,"NODENAME":"測試深度節(jié)點200","FLAG":"1","HASCHILD":"1","PARENTNODE":"1000200000","LEVELCODE":"100020000000000"}
,{"NODELEVEL":4,"NODENAME":"測試深度節(jié)點2000","FLAG":"1","HASCHILD":"1","PARENTNODE":"100020000000000","LEVELCODE":"10002000000000000000"}
,{"NODELEVEL":5,"NODENAME":"測試深度節(jié)點20000","FLAG":"1","HASCHILD":"1","PARENTNODE":"10002000000000000000","LEVELCODE":"1000200000000000000000000"}
,{"NODELEVEL":6,"NODENAME":"qwerfga","FLAG":"1","HASCHILD":"0","PARENTNODE":"1000200000000000000000000","LEVELCODE":"100020000000000000000000000001"}
,{"NODELEVEL":2,"NODENAME":"sdfg","FLAG":"1","HASCHILD":"1","PARENTNODE":"10002","LEVELCODE":"1000200001"}
,{"NODELEVEL":3,"NODENAME":"safsdfsadfaaa","FLAG":"1","HASCHILD":"0","PARENTNODE":"1000200001","LEVELCODE":"100020000100001"}
]
通常樹需要的數(shù)據(jù)是有結(jié)構(gòu)層次的,形如:
[
{
"NODELEVEL":2,
"NODENAME":"sdfg",
"FLAG":"1",
"HASCHILD":"1",
"PARENTNODE":"10002",
"LEVELCODE":"1000200001",
"children":[
{
"NODELEVEL":3,
"NODENAME":"safsdfsadfaaa",
"FLAG":"1",
"HASCHILD":"0",
"PARENTNODE":"1000200001",
"LEVELCODE":"100020000100001"
}
],
"isFolder":true
},
{
"NODELEVEL":2,
"NODENAME":"測試深度節(jié)點20",
"FLAG":"1",
"HASCHILD":"1",
"PARENTNODE":"10002",
"LEVELCODE":"1000200000",
"children":[
{
"NODELEVEL":3,
"NODENAME":"測試深度節(jié)點200",
"FLAG":"1",
"HASCHILD":"1",
"PARENTNODE":"1000200000",
"LEVELCODE":"100020000000000",
"children":[
{
"NODELEVEL":4,
"NODENAME":"測試深度節(jié)點2000",
"FLAG":"1",
"HASCHILD":"1",
"PARENTNODE":"100020000000000",
"LEVELCODE":"10002000000000000000",
"children":[
{
"NODELEVEL":5,
"NODENAME":"測試深度節(jié)點20000",
"FLAG":"1",
"HASCHILD":"1",
"PARENTNODE":"10002000000000000000",
"LEVELCODE":"1000200000000000000000000",
"children":[
{
"NODELEVEL":6,
"NODENAME":"qwerfga",
"FLAG":"1",
"HASCHILD":"0",
"PARENTNODE":"1000200000000000000000000",
"LEVELCODE":"100020000000000000000000000001"
}
],
"isFolder":true
}
],
"isFolder":true
}
],
"isFolder":true
}
],
"isFolder":true
}
]
//ps:感謝
http://jsonformatter.curiousconcept.com/ 在線format json
從平鋪的格式轉(zhuǎn)換到層次結(jié)構(gòu)的json我經(jīng)過多次實踐,得出了下面的方法:
最開始,我約定ajax請求能得到樹深度遍歷結(jié)果,推薦之前一篇文件:
一種能跨數(shù)據(jù)庫的樹形數(shù)據(jù)表格設(shè)計 上面的示例數(shù)據(jù)就是一個深度遍歷結(jié)果。深度遍歷結(jié)果的特點是每條記錄的上一條記錄要么是兄弟節(jié)點,要么是父節(jié)點,每條記錄的下一條記錄要么是兄弟節(jié)點,要么是子節(jié)點。根據(jù)這個特點,可以用如下方法解析:
function parse(arr, option) {
var treenodes = [];//存放解析結(jié)果
var stack = [];//存放當(dāng)前節(jié)點路徑 如果A->B->C-D 遍歷到D節(jié)點時stack應(yīng)該是[A,B,C]
for ( var i = 0; i < arr.length; i++) {
var mapednode = arr[i];
if (i == 0) {// 第一個節(jié)點
treenodes.push(mapednode);
stack.push(mapednode);
} else {
var previousnode = arr[i - 1];
if (parseInt(previousnode.level, 10) + 1 == parseInt(mapednode.level, 10)) {// 深度增加(深度增加1才視為深度增加)
var parentnode = stack[stack.length - 1];//棧的最后一個節(jié)點是父節(jié)點
parentnode.isFolder = true;
if (!parentnode.children) parentnode.children = [];
parentnode.children.push(mapednode);
stack.push(mapednode);
}
if (previousnode.level == mapednode.level) {// 與之前節(jié)點深度相同
// 棧中最后一個節(jié)點此時是同級節(jié)點previousnode
// 所以此處去父節(jié)點時需要取到previousnode的父節(jié)點
// 如果是一級節(jié)點,此時父節(jié)點應(yīng)該是根節(jié)點
if (stack.length > 1) {
var parentnode = stack[stack.length - 2];
parentnode.isFolder = true;
if (!parentnode.children)
parentnode.children = [];
parentnode.children.push(mapednode);
} else {
treenodes.push(mapednode);
}
stack[stack.length - 1] = mapednode;// 保證棧中是同級節(jié)點的最后一個
}
if (previousnode.level > mapednode.level) {// 深度減少 出棧
for ( var j = 0; j < (previousnode.level - mapednode.level); j++) {
stack.pop();
}
// 如果回到了一級節(jié)點,此時父節(jié)點應(yīng)該是根節(jié)點
if (stack.length > 1) {
var parentnode = stack[stack.length - 2];
parentnode.isFolder = true;
if (!parentnode.children) {
parentnode.children = [];
}
parentnode.children.push(mapednode);
} else {
treenodes.push(mapednode);
}
stack[stack.length - 1] = mapednode;// 保證棧中是同級節(jié)點的最后一個
}
}
}
delete stack;
delete arr;
return treenodes;
}
var parseoption={
titlefield:"NODENAME",//顯示名稱字段
keyfield:"LEVELCODE",//節(jié)點唯一標(biāo)識字段
levelfield:"NODELEVEL",//深度字段
parentfield:"PARENTNODE",//父節(jié)點標(biāo)識字段
customsiblingsort:"NODENAME"//同級節(jié)點排序字段
};
var parsedTreeData = parse(arr,parseoption);
基本思路是將arr循環(huán),然后將節(jié)點層級的“塞”到對應(yīng)的地方。因為總是要跟前一個節(jié)點數(shù)據(jù)比較,所以需要訪問到上一個節(jié)點。而且總需要獲得當(dāng)前節(jié)點的父節(jié)點,而且需要判斷是不是回到了一級節(jié)點,所以需要記錄當(dāng)前節(jié)點的訪問路徑,從整個深度遍歷arr的過程看,當(dāng)前節(jié)點的路徑剛好是一個先進先出的關(guān)系,所以將stack聲明為一個堆棧使用。
上面的方法沒有實現(xiàn)同級節(jié)點排序,使用push方法能保證樹的同級顯示順序與深度遍歷結(jié)果中的出現(xiàn)順序(這樣可以利用ajax數(shù)據(jù)本身的順序)。其實可以用
插入排序的方法來替換上面的多處push。
//插入排序法
var cl= parentnode.children.length;
var insertIndex = 0;
for ( var j = 0; j < cl; j++) {
var targetSortValue = arr[parentnode.children[j]][fSIBLINGSORT]||"";
//字符串比較
if(isNaN(targetSortValue) && isNaN(sortValue) && targetSortValue.localeCompare(sortValue)>0 ){
insertIndex= j;
break;
}else{
insertIndex=j+1;
}
//數(shù)字比較
if((!isNaN(targetSortValue)) && (!isNaN(sortValue)) && parseFloat(targetSortValue)>parseFloat(sortValue) ){
insertIndex= j;
break;
}else{
insertIndex=j+1;
}
}
parentnode.children.splice(insertIndex, 0, i);
但是事實上用戶老會抱怨為什么一定要深度遍歷的結(jié)果,雖然,在之前提到的
一種跨數(shù)據(jù)庫的數(shù)據(jù)表設(shè)計中比較容易獲取到深度遍歷結(jié)果。
所以我又開始想怎么將無序的平鋪樹數(shù)據(jù)解析成層級結(jié)構(gòu)。
function parse(arr,option){
//有同級排序 拼接同級節(jié)點排序函數(shù)
if(option.customsiblingsort){
if(typeof(window[option.customsiblingsort])!="function"){//不是函數(shù),視為字段名
var sortfield = option.customsiblingsort;
option.customsiblingsort= function(node1,node2){
var v1 =node1[sortfield]||"";
var v2 =node2[sortfield]||"";
if((!isNaN(v1)) && (!isNaN(v2))){//數(shù)字比較
return parseFloat(v1)- parseFloat(v2);
}else{
return v1.localeCompare(v2);//字符串比較
}
};
}else{
option.customsiblingsort=window[option.customsiblingsort];
}
}
//step1:排序,按照節(jié)點深度排序,同深度節(jié)點按照customsiblingsort排序
arr.sort(function (node1,node2){
if((node1[option.levelfield]-node2[option.levelfield])>0){
return 1;
}else if ((node1[option.levelfield]-node2[option.levelfield])==0 && node1[option.parentfield].localeCompare(node2[option.parentfield])>0){
return 1;
}else if ( option.customsiblingsort && (node1[option.levelfield]-node2[option.levelfield])==0 && node1[option.parentfield].localeCompare(node2[option.parentfield])==0 && option.customsiblingsort(node1,node2)>0 ){
return 1;
}else{
return 0;
}
});
var result =[];
var mapnode ={};//key :node key,value:node object reference
//step2:排序后節(jié)點解析成樹
for(var i= 0;i<arr.length;i++){
var mapednode =arr[i];
var n = mapednode;
//深度為1或者深度與排序后第一個節(jié)點的深度一致視為一級節(jié)點
if(n[option.levelfield]==1 || n[option.levelfield]==arr[0][option.levelfield]){
result.push(n);
mapnode[n[option.keyfield]]= n;
continue;
}
if(n[option.levelfield]>=1){
//找到父節(jié)點
mapnode[n[option.keyfield]]= n;
var p= n[option.parentfield];
var parentNode = mapnode[p];
//arr是按照深度排序,任何一個節(jié)點的父節(jié)點都會在子節(jié)點之前,所以parentNode!=null
if(typeof(parentNode.children)=="undefined"){
parentNode.children=[];
}
parentNode.isFolder=true;
parentNode.children.push(n);
}
}
delete arr,mapnode;
return result;
}
var parseoption ={titlefield:"NODENAME",keyfield:"LEVELCODE",levelfield:"NODELEVEL",parentfield:"PARENTNODE",customsiblingsort:"NODENAME"};
var parsedTreeData = parse(arr,parseoption);
這種方法的循環(huán)次數(shù)自然比第一種方法要長,先根據(jù)深度將數(shù)組排序,這樣做的目的一個是保證在遍歷數(shù)組的任何一個節(jié)點是能在之前的節(jié)點中找到其父節(jié)點,第二也附帶著將同級節(jié)點的排序也實現(xiàn)了。這種方法代碼可讀性比一種直接,性能慢在第一階段的排序上。
樹的前端顯示總是能對前端開發(fā)者造成很大的挑戰(zhàn),光解析數(shù)據(jù)就會費勁心思。為了不至于要使用xtree,dtree那樣的傳統(tǒng)的tree控件,還是找找jQuery插件。目前發(fā)現(xiàn)了兩個不錯的jQuery tree插件
jsTree 和
dynatree 。不過都版本還是比較低,都不是很穩(wěn)定,經(jīng)常會有bug。jsTree就支持無序的數(shù)組數(shù)據(jù)源,dynatree必須是層次數(shù)據(jù)源。dynatree的性能顯然要比jsTree要好,支持lazyLoad。所以選定dynatree。只是需要自己解析數(shù)據(jù)。
大家在樹方面有什么建議和經(jīng)驗不妨分享分享。
相關(guān)的代碼下載