打造jQuery的高性能TreeView

UPDATE:回答網友提出的設置節點的自定義圖片的問題,同時歡迎你們提問,我儘可能在第一時間回覆,詳見最後 2009-11-03javascript

項目中常常會遇到樹形數據的展示,包括導航,選擇等功能,因此樹控件在大多項目中都是必須的。那一個實用的樹應該具有什麼功能呢?css

根據個人項目實踐狀況,主要是幾個關鍵點:html

1:支持靜態的樹,即一次性將所有數據加載到客戶端。java

2:異步樹,即一次只加載一級或若干級節點,子節點能夠異步加載數據。node

3:Checkbox樹(多是靜態樹也多是異步樹),用於選擇(如選擇組織機構,選擇數據字典項)等,最好是可以支持節點級聯(這個是難點)jquery

4:可以承載大數據量,並性能表現優異web

5:可以在主流瀏覽器中運行良好ajax

那我要打造的TreeView就是爲了實現這個5個主要指標的。json

先來看下效果圖windows

image

上圖是中國行政區域的數據樹,總共得節點是3500+。

那麼咱們要開工了;

1:第一個肯定的節點Dom結構(即用什麼樣的HTML來構建節點)

  • 比較土的是table套table的(樣式上好控制,可是大數據量,和層次較深的樹,這種結構確定頂不住的)
  • 還有一種是比較新鮮的UL套LI的方式,這是現下不少書採起的方式如Jquery.treeview就是採用的這種格式,好處比較明顯就是結構簡潔明瞭,
    並且在不支持Js的瀏覽器上,一樣也能呈現出樹的形狀(這種狀況其實咱能夠忽略),可是Jquery.treeview的節點在IE下,特別是IE6下沒法被內部元素撐開,(IE7,8當達到必定深度時沒法撐開),請奇怪的現象(我猜想是由於使用padding來作縮進,margin-left:負值來控制圖標位置有關,可是修改起來難度也較大),在這種狀況下書會變形(Jquery.treeview)就有這種問題,只能經過設置節點的width來解決。

image

JQuery.treeview的節點結構

image

Jquery.TreeView  IE6 下 展開第三級即出現錯位

image IE8下展開到第5級

  • 還有一些是div套table的方式,CSDN的導航樹就是這種,是種折中的方法(節點也不算太複雜,並且CSS也比較好寫),以下圖所示
    image

 

 

 

而我採用的也是第二種方式,可是縮進採用了填空的方式,即縮進的位置用空白的圖片填充來避免Jquery.treeview的問題

image

個人樹節點結構

肯定了節點的HTML咱們就能夠來寫CSS了。有了效果圖,有個節點結構接着就編寫CSS了

下面是CSS的完整代碼

上面了樹的基本樣式外,定義了一個有+號帶line的樣式和+號不帶line的樣式

image 這就是那個+號帶line的樣式

css中所用到的全部圖片

arrows elbow elbow-end elbow-end-minus elbow-end-minus-nl elbow-end-plus elbow-end-plus-nl elbow-line elbow-minus elbow-minus-nl elbow-plus elbow-plus-nl folder folder-open leaf loading

checkbox_2 checkbox_0 checkbox_1

2:肯定數據結構

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var menudata = [{
     id: "0.1" , //惟一的ID便可
     text: "Beyondbit UI Demo" ,
     hasChildren: true ,
     isexpand: true ,
     complete: true ,
     ChildNodes: [{
         id: "0.1.1" ,
         text: "日期選擇" ,
         hasChildren: true ,
         isexpand: false ,
         complete: true ,
         ChildNodes: [{
             id: "0.1.1.1" ,
             text: "控件演示" ,
             value: "Testpages/datepickerDemo.htm" ,
             hasChildren: false ,
             isexpand: false ,
             complete: true ,
             ChildNodes: null
         },
         ...
         ]

 

 

這樣的結構有個好處就數據自己是帶層次的,很是利於遍歷,在後面的級聯關聯中會看到


3: 面子作好了那就開始作裏子了,編寫腳本(Javascript)

我是JQuery得擁護者,因此天然js的框架天然是採用Jquery了

先上個完整代碼,再逐一分析

 

第一步:天然是全部Jquery的控件的第一步都是搭這個架子,兼容JQuery和$避免閉包,避免和其餘類庫衝突,接受一個參數(是個對象)

?
1
2
3
4
5
6
;( function ($) {
      //也可使用$.fn.extend(treeview:function(setting){})
     $.fn.treeview = function (settings) {
     }
 
})(jQuery);
那第二步:給控件加一些參數默認參數,同時能調用方法$.extend讓最終調用時的參數覆蓋默認的(若是沒有則使用默認)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var dfop ={
                 method: "POST" , //默認採用POST提交數據
                 datatype: "json" , //數據類型是json
                 url: false , //異步請求的url
                 cbiconpath: "/images/icons/" , //checkbox icon的目錄位置
                 icons: [ "checkbox_0.gif" , "checkbox_1.gif" , "checkbox_2.gif" ], //checkbxo三態的圖片
                 showcheck: false , //是否顯示checkbox         
                 oncheckboxclick: false , //點擊checkbox時觸發的事件
                 onnodeclick: false , //點擊node觸發的時間
                 cascadecheck: true , //是否啓用級聯
                 data: null , //初始化數據            
                 theme: "bbit-tree-arrows" //三種風格備選bbit-tree-lines ,bbit-tree-no-lines,bbit-tree-arrows
             }
         //用傳進來的參數覆蓋默認,沒傳則保留
         $.extend(dfop, settings);
第三步:生成默認數據的HTML(根據咱們的分析節點的Dom結構,數據的數據結構,生成節點那是很是的簡單),,添加到當前容器中。最後是註冊事件 這裏有一個很是重要的地方,即懶加載(沒有展開的節點HTML是不生成的),這就要求咱們在樹內部要維護一套數據(開銷很小),對於性能的提高那是至關的明顯。 另一個重要的地方,就是使用一次生成全部展開節點的HTML並經過innerHTML屬性來生成Dom,而不是經過append操做,由於直接操做innerHTML比經過dom原生的 方法要快上N倍(節點越多,N越大),切記切記!
?
1
2
3
4
5
6
7
8
9
10
11
12
13
var treenodes = dfop.data; //內部的數據,其實直接用 dfop.data也能夠
var me = $( this );
var id = me.attr( "id" );
if (id == null || id == "" ) {
     id = "bbtree" + new Date().getTime();
     me.attr( "id" , id);
} //全局惟一的ID
 
var html = [];
buildtree(dfop.data, html); //生成展開節點的HTML,push到數組中
me.addClass( "bbit-tree" ).html(html.join( "" ));
InitEvent(me); //初始化事件
html = null ;
在節點生成過程當中,同時可生產節點的Path(節點路徑),方便檢索
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (nd.hasChildren) { //存在子節點
     if (nd.isexpand) { //同時節點已經展開則輸出子節點
         ht.push( "<ul  class='bbit-tree-node-ct'  style='z-index: 0; position: static; visibility: visible; top: auto; left: auto;'>" );
         if (nd.ChildNodes) {
             var l = nd.ChildNodes.length;
             for ( var k = 0; k < l; k++) { //遞歸調用並生產節點的路徑
                 nd.ChildNodes[k].parent = nd;
                 buildnode(nd.ChildNodes[k], ht, deep + 1, path + "." + k, k == l - 1);
             }
         }
         ht.push( "</ul>" );
     }
     else { //不然是待輸出狀態
         ht.push( "<ul style='display:none;'></ul>" );
     }
}
註冊事件,接受參數parent,即從某一父節點開始附加事件(由於作了個hover效果,因此事件是在每一個節點上,若是取消該效果,事件可直接附加Tree上經過Event的srcElement來分發可略提高性能)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function InitEvent(parent) {
           var nodes = $( "li.bbit-tree-node>div" , parent);
           nodes.each( function (e) {
               $( this ).hover( function () {
                   $( this ).addClass( "bbit-tree-node-over" ); //鼠標浮動節點的樣式變化
               }, function () {
                   $( this ).removeClass( "bbit-tree-node-over" );
               })
               .click(nodeclick) //node的onclick事件,這個是重點哦
               .find( "img.bbit-tree-ec-icon" ).each( function (e) { //arrow的hover事件,爲了實現vista那個風格的
                   if (!$( this ).hasClass( "bbit-tree-elbow" )) {
                       $( this ).hover( function () {
                           $( this ).parent().addClass( "bbit-tree-ec-over" );
                       }, function () {
                           $( this ).parent().removeClass( "bbit-tree-ec-over" );
                       });
                   }
               });
           });
}
這裏最主要的仍是node的click事件,由於他要處理的事情不少,如樹的展開收縮(若是子節點不存在,可是hasChildren爲真,同時complete屬性不爲真則須要異步加載子節點,如子節點存在,可是沒有Render那麼就要Render),點擊checkbox要出發級聯的事件和oncheckbox事件,點擊其餘則觸發配置條件的nodeonclick事件,這一切都經過前面event的源元素的class來區分點擊的對象
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
   function nodeclick(e) {
     var path = $( this ).attr( "tpath" ); //獲取節點路徑
     var et = e.target || e.srcElement; //獲取事件源
     var item = getItem(path); //根據path獲取節點的數據
     //debugger;
     if (et.tagName == "IMG" ) {
         // +號須要展開,處理加減號
         if ($(et).hasClass( "bbit-tree-elbow-plus" ) || $(et).hasClass( "bbit-tree-elbow-end-plus" )) {
             var ul = $( this ).next(); //"bbit-tree-node-ct"
             if (ul.hasClass( "bbit-tree-node-ct" )) {
                 ul.show();
             }
             else {
                 var deep = path.split( "." ).length;
                 if (item.complete) {
                     item.ChildNodes != null && asnybuild(item.ChildNodes, deep, path, ul, item);
                 }
                 else {
                     $( this ).addClass( "bbit-tree-node-loading" );
                     asnyloadc(ul, item, function (data) {
                         item.complete = true ;
                         item.ChildNodes = data;
                         asnybuild(data, deep, path, ul, item);
                     });
                 }
             }
             if ($(et).hasClass( "bbit-tree-elbow-plus" )) {
                 $(et).swapClass( "bbit-tree-elbow-plus" , "bbit-tree-elbow-minus" );
             }
             else {
                 $(et).swapClass( "bbit-tree-elbow-end-plus" , "bbit-tree-elbow-end-minus" );
             }
             $( this ).swapClass( "bbit-tree-node-collapsed" , "bbit-tree-node-expanded" );
         }
         else if ($(et).hasClass( "bbit-tree-elbow-minus" ) || $(et).hasClass( "bbit-tree-elbow-end-minus" )) {  //- 號須要收縮                   
             $( this ).next().hide();
             if ($(et).hasClass( "bbit-tree-elbow-minus" )) {
                 $(et).swapClass( "bbit-tree-elbow-minus" , "bbit-tree-elbow-plus" );
             }
             else {
                 $(et).swapClass( "bbit-tree-elbow-end-minus" , "bbit-tree-elbow-end-plus" );
             }
             $( this ).swapClass( "bbit-tree-node-expanded" , "bbit-tree-node-collapsed" );
         }
         else if ($(et).hasClass( "bbit-tree-node-cb" )) // 點擊了Checkbox
         {
             var s = item.checkstate != 1 ? 1 : 0;
             var r = true ;
             if (dfop.oncheckboxclick) { //觸發配置的函數
                 r = dfop.oncheckboxclick.call(et, item, s);
             }
             if (r != false ) { //若是返回值不爲false,即checkbxo變化有效
                 if (dfop.cascadecheck) { //容許觸發級聯
                     //遍歷
                     cascade(check, item, s); //則向下關聯
                     //上溯
                     bubble(check, item, s); //向上關聯
                 }
                 else {
                     check(item, s, 1); //不然只管本身
                 }
             }
         }
     }
     else { //點擊到了其餘地方
         if (dfop.citem) { //上一個當前節點
             $( "#" + id + "_" + dfop.citem.id).removeClass( "bbit-tree-selected" );
         }
         dfop.citem = item; //此次的當前節點
         $( this ).addClass( "bbit-tree-selected" );
         if (dfop.onnodeclick) {
             dfop.onnodeclick.call( this , item);
         }
     }
}

展開節點,異步請求的部分代碼應該不是很複雜就不細訴了,關鍵來說一下級聯
級聯有兩個問題要處理,第一個是遍歷子節點,第二個是上溯到祖節點,由於咱們的數據結構這兩個操做都顯得很是簡單

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//遍歷子節點
function cascade(fn, item, args) {
     if (fn(item, args, 1) != false ) {
         if (item.ChildNodes != null && item.ChildNodes.length > 0) {
             var cs = item.ChildNodes;
             for ( var i = 0, len = cs.length; i < len; i++) {
                 cascade(fn, cs[i], args);
             }
         }
     }
}
//冒泡的祖先
function bubble(fn, item, args) {
     var p = item.parent;
     while (p) {
         if (fn(p, args, 0) === false ) {
             break ;
         }
         p = p.parent;
     }
}

找到節點的同時都會觸發check這個回調函數,來判斷當前節點的狀態,詳細請看下面代碼中的註釋部分應該是比較清晰,描寫了這個過程

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
     function check(item, state, type) {
     var pstate = item.checkstate; //當前狀態
     if (type == 1) {
         item.checkstate = state; //若是是遍歷子節點,父是什麼子就是什麼
     }
     else { // 上溯 ,這個就複雜一些了
         var cs = item.ChildNodes; //獲取當前節點的全部子節點
         var l = cs.length;
         var ch = true ; //是否不是中間狀態 半選
         for ( var i = 0; i < l; i++) {
             if ((state == 1 && cs[i].checkstate != 1) || state == 0 && cs[i].checkstate != 0) {
                 ch = false ;
                 break ; //他的子節點只要有一個沒選中,那麼他就是半選
             }
         }
         if (ch) {
             item.checkstate = state; //不是半選,則子節點是什麼他就是什麼
         }
         else {
             item.checkstate = 2; //半選
         }
     }
     //change show 若是節點已輸出,而其先後狀態不同,則變化checkbxo的顯示        
     if (item.render && pstate != item.checkstate) {
         var et = $( "#" + id + "_" + item.id + "_cb" );
         if (et.length == 1) {
             et.attr( "src" , dfop.cbiconpath + dfop.icons[item.checkstate]);
         }
     }
}

至此咱們樹的主體功能已經徹底實現了。其餘就是公開一些方法等,你們可詳見代碼,示例中公開了兩個一個當前選中的全部節點,另一個當前的節點。 

你們能夠經過如下網址查看文中的示例,selected拼錯了,你們海涵! windows azure部署仍是麻煩懶得修改了3500+節點一次加載,你們能夠點擊根節點的全選來看看速度

 http://jscs.cloudapp.net/ControlsSample/BigTreeSample 

異步加載,按需加載的狀況也是很是經常使用的,使用的是SQL Azure服務器在美國ing,因此可能異步有點慢,本地數據源那是瞬間的

http://jscs.cloudapp.net/ControlsSample/TreeAsnySample

FAQ:

1:如何設置每一個節點不一樣的圖標?

回答:

其實不用擴展,自己就支持,只是沒有說明而已,咱們來看一下這個代碼吧?在BuildNode方法中有這麼一句?      
if (nd.classes) { cs.push(nd.classes); }

在節點的數據結構中能夠設置屬性classes ,該屬性將做爲節點特殊的Css Class 添加到節點上。那麼利用這點,就能夠設置節點的圖標了

image

而後就是編寫一個Style 便可

image

最後來看下效果吧?

image

 

 

 出處:http://www.cnblogs.com/xuanye/archive/2009/10/26/xjplugin_jquery_tree.html

 demo下載地址: demo 

相關文章
相關標籤/搜索