Dojo Grid 結構
Dojo Grid 在結構上有點類似於大家熟悉的 MVC 模式。MVC 模式是“Model-View-Controller”的縮寫,也就是“模型 - 視圖 - 控制器”。
圖 1.MVC 結構

一個最簡單的 Grid 在結構上主要有以下幾方面構成:
模型 (Model)
每個 Grid 都會包含數據,所以每個 Grid 開頭都會去定義 Model。如清單 1 中的定義,Model 包含了 dojotype(dojo 模型類),jsid(專用 id),structure(結構),Store(數據庫)等。 其中比較重要的部分是 Store,它放置了 Grid 中存儲的數據。
清單 1. Grid 的定義
<div id="grid1" dojotype="dojox.grid.DataGrid" jsid="modelGrid" rowselector="0px" canSort="false" structure="modelGridLayout" Store="modelStore"></div>
視圖 (View) 和結構 (Structure)
View 用來定義每個數據列,一個 View 是多個數據列的組合。通過定義 View,使 Grid 按照要求來顯示數據。 Structure 是 View 的集合,也就是說可以將多個 View 組合成一個 Structure。Structure 會被 Grid 用到,而 View 不會被 Grid 直接用到, 而是被包裝成一個 Structure 來使用。清單 2 中是一個 Grid Layout 的范例,它定義了 Grid 的結構。cells 部分定義了 Grid 列定義的信息。 每一列需要定義 name、id、field,以及列的 html 形式如長寬高之類的。之后對 Grid 列的操作主要是針對 field 域。
清單 2. Grid Layout 的定義
ModelGridLayout = [{ cells: [ { name:'<div style="width:20px;height:20px;"><input type="checkbox" onclick="DeviceGridRevertSelectAll(this)" id="checkcollection"></div>', field: 'Sel', editable: true, width: '20px', cellStyles: 'text-decoration: none; cursor:default; text-align: center;position: relative; left: -10px', headerStyles: 'text-align: center;', type: dojox.grid.cells.Bool }, { name: 'Model',field: 'Model', width: '170px',cellStyles:'font-size:9pt; cursor: default;text-align: left;', cellClasses: 'defaultColumn', headerStyles: 'text-align: center;'}, { name: 'Device',field: 'Device', width: '150px', cellStyles: 'font-size: 9pt; font-style:normal,text-decoration: none; cursor:default;text-align: left;', cellClasses: 'defaultColumn', headerStyles: 'text-align: center;'}, ] }];
Grid 控件 (Widget)
這里的 Grid 控件類似於 MVC 中的控制器(Control)。通過 Grid 各種預先定義的 API 對 Grid 的數據(Model), 視圖(View)有效的組織起來,並進行操作。以達到有效控制 Grid 數據存取、更新、外觀變化的目的。 從而顯示出一個類似於電子表格的 Grid 列表。
Dojo Grid 的數據存儲
在 Grid Model 的定義中,有一個叫 Store 的屬性,它存儲了與 Grid 相關聯的數據,也就是 Grid 綁定到的數據源。 在示例中,數據源的名字叫 modelStore。modelStore 的定義如下:
<div dojotype="dojo.data.ItemFileWriteStore" jsid="modelStore" url="data/modelItemList.json"></div>
Dojo 的核心提供了一個只讀的數據體實現,ItemFileReadStore。這個數據體可以從 HTTP 端點讀取 Json 結構體,或是讀取內存中的 JavaScript 對象。 Dojo 允許為 ItemFileReadStore 指定某個屬性來作為 identifier(唯一標識符)。Dojo 內核同時提供了 ItemFileWriteStore 存儲作為 ItemFileReadStore 的一個擴展,它是建立在 dojo.data.api.Write 和 dojo.data.api.Notification API 對 ItemFileReadStore 的支持之上的。如果你的應用程序需要 寫入數據到 ItemFileStore,則 ItemFileWriteStore 正是你所要的。
對於 Store 的操作,可以使用函數 newItem, deleteItem, setValue 來修改 Store 的內容,這些操作可以通過調用 revert 函數來取消, 或者調用 save 函數來提交修改。
在使用這些函數時,一定要注意的是,如果你為 ItemFileWriteStore 指定了某個屬性來作為 identifier,必須要確保它的值是唯一的, 這對 newItem 和 deleteItem 函數特別重要,ItemFileWriteStore 使用這些 ID 來跟蹤這些改變,這意味着即使你刪除了一個 Item, 它的 identity 還是被保持為使用狀態,因此,如果你調用 newItem() 並試圖重用這個 identifier,你將得到一個異常。要想重用這個 identifier, 你需要通過調用 save() 來提交你的改變。Save 將應用所有當前的改變並清除掛起的狀態,包括保留的 identifier。當你沒有為 Store 指定 identifier 時, 則不會出現上述問題。原因是,Store 會為每個 Item 自動創建 identifier,並確保了它的值是唯一的。在這種自動創建 identifier 的情況下, identifier 是不會作為一個公有屬性暴露出來的(它也不可能通過 getValue 得到它,僅 getIdentity 可以)。
Store 的數據存儲在兩個 json 數組中,名字分別為 _arrayOfAllItems 和 _arrayOfTopLevelItems。這兩個數組的區別是在於前者記錄了 Grid 創建以來 Store 中存在過的所有變量, 后者中只是存儲 Store 當前的所有 Item。如果有變量被刪除,則 _arrayOfAllItems 中該數組變量設為 null,而 _arrayOfTopLevelItems 中該數組變量會被徹底刪除, 數組個數減一。這樣的設定是為了在 Store.newItem 的時候,如果用戶沒有為 Store 指定 identifier,Store 可以自動地用 _arrayOfAllItems 的數量值為新 Item 創建 identifier。_arrayOfAllItems 的個數不會因為刪除操作而減少,也就不必擔心新 Item 的 identifier 就會發生重復。
程序清單 3 中描述了 Grid 自動創建 identifier 的過程,首先程序會嘗試去獲得之前定義的 Identifier 屬性。如果屬性是 Number 就把 _arrayOfAllItems.length 賦給 newIdentity。如果屬性不是 Number, 就把 identifierAttribute 對應的 keywordArgs 賦給 newIdentity,如果 newIdentity 是空,就表示 identity 創建失敗。
清單 3. Grid identifier 的創建
var newIdentity = null; var identifierAttribute = this._getIdentifierAttribute(); if(identifierAttribute === Number){ newIdentity = this._arrayOfAllItems.length; } else{ newIdentity = keywordArgs[identifierAttribute]; if (typeof newIdentity === "undefined"){ throw new Error("newItem() was not passed an identity for the new Item"); } }
圖 2 則顯示了當刪除一個數據后,_arrayOfAllItems 和 _arrayOfTopLevelItems 的差別。_arrayOfAllItems 有個變量被置空,而 _arrayOfTopLevelItems 的個數減一了。
圖 2. arrayOfAllItems 和 arrayOfTopLevelItems

Dojo Grid 性能問題
Dojo Grid 了提供大量非常實用的 API,基於這些函數,程序員可以制作出漂亮的電子表格。但是在使用過程中, 它的一些性能問題就逐漸暴露出來。有個比較突出的問題就是在使用 newItem() 方法往 Grid 中添加大量數據的時候, 瀏覽器會因為 Grid 的操作而陷入忙碌,很長時間沒有響應甚至白屏。而對於 UI 用戶而言,快速穩定的頁面響應是 他們所共同期望的。那么有沒有什么辦法能改善這一問題呢?
導致性能問題的原因
圖 3.Grid 修改數據

Grid 修改數據的過程總是先修改 Grid 綁定到的 Store 中的數據,然后再根據需要,改變 Grid 的 View,完成 Grid 外表的更新。 在通常情況下,在 Grid 創建之時,如果定義了 Store,Grid 就會調用 this._setStore(this.Store); 來對自身的 Store 屬性進行配置。 在 _setStore 這個函數里,Grid 對 Store 創建、刪除、修改 Item 事件進行偵聽。Grid 使用 dojo.connect() event model 來綁定一個自定義 的函數到 Store 上,當無論何時 Store 調用 onSet, onNew, and onDelete 時,都將調用這個函數。這個過程就如清單 4 中所描述的那樣。 Grid 通過 this.connect 把 Store 的 onSet, onNew, and onDelete 事件和自身定義的 _onSet._onNew,_onDelete 鏈接起來了。
清單 4. Grid connect event
h.push(this.connect(this.Store, "onSet", "_onSet")); h.push(this.connect(this.Store, "onNew", "_onNew")); h.push(this.connect(this.Store, "onDelete", "_onDelete"));
清單 5 展示了 Store 的 _onNew 事件處理函數。當程序使用 newItem 往 Store 里增加數據時 , Store 在完成添加操作后會發出一個 dojo.data.api.Notification —— this.onNew(newItem, pInfo); Grid 監聽到了這個 Notification 后就會調用之前定義過的 _onNew 處理函數,對自身進行操作。Grid 會分別更新自己的行數,增加新項目 (_addItem),如有需要打印一些消息。
清單 5. Store _onNew()
_onNew: function(Item, parentInfo){ this.updateRowCount(this.rowCount+1); // 更新行數 this._addItem(Item, this.rowCount-1); // 增加新 Item this.showMessage(); // 打印某些信息 }
在上面的三個步驟中,增加新的 Item 是最關鍵的。_addItem 的操作包括了獲得新項目的 Identity,分配 Identity 空間,向 Identity 空間填充 Item,以及更新行視圖(添加新的 dom 節點)。具體過程如清單 6 中所示。
清單 6. Grid _addItem()
_addItem: function(Item, index, noUpdate){ var idty = this._hasIdentity ? this.Store.getIdentity(Item) : dojo.toJson(this.query) + ":idx:" + index + ":sort:" + dojo.toJson(this.getSortProps()); // 獲得 Identity var o = { idty: idty, Item: Item }; // 分配獲得空間 this._by_idty[idty] = this._by_idx[index] = o;// 向 Identity 空間填充 Item if(!noUpdate){ this.updateRow(index);// 更新行視圖 } }
通過上面的分析,我們可以看到,如果使用 newItem 循環往 Store 中添加數據,那么 Store 在執行完一次 add 的操作后都會引發 Grid 去載入新加項目, 並更新自己的視圖。經過實驗發現,往 Store 中添加 Item 的速度是很快的,而 grid 的載入新加項和更新自身視圖的操作是比較慢的。每次新加一個 數據,Grid 都會嘗試把新加的數據都加載進來(創建所有的 dom node),雖然這保證了 Grid 獲得 Store 當前的所有數據,但是這種操作加大了瀏覽 器的內存開銷,更進一步使瀏覽器陷入忙碌,長時間沒有響應。
Grid 性能問題的解決方法
要解決上述性能問題,需要解決三個方面的問題。
Step 1. 適時斷開 Grid 和 Store 的連接
因為往 Store 中添加數據的速度很快,而 Grid 的載入新加項和更新視圖速度較慢,所以在操作之初,就把 Grid 的 Store 設置為空(null), 斷開 Grid 和 Store 的連接 , deviceGrid._setStore(null);。這樣即使 Store 因為 addItem 發生變化,也不會聯動引起 Grid 的操作了。 當 Store 中添加完數據后再把 Grid 和 Store 鏈接上:deviceGrid._setStore(deviceStore); 最后把數據重新加載完成視圖更新:deviceGrid.render()。 清單 7 中展示的就是這個過程,在添加數據之初 modelGrid 先斷開 Store 的連接,再往 modelStore 中添加數據,結束后又把 modelStore 重新連接上 modelGrid.
清單 7. Grid 添加項目
function _addGridData(dataarray) { modelGrid._setStore(null); // 斷開連接 /* 往 Grid 中添加數據 */ for(var i=0;i<datalength;i++){ var modelname = dataarray[i].split(',')[0]; var devicename = dataarray[i].split(',')[1]; modelStore.newItem({id:"modelItem"+i,StatusImage:'<img src="images/Normal_obj16.gif">', Sel:true,Loop:1,Status:'<img src="images/statusStopped_obj16.gif">Not Started', Model: modelname, Device: devicename}); } modelGrid._setStore(modelStore);// 恢復連接 modelGrid.render();// 重新加在視圖 }
Step2. 適應“lazy loading”數據加載機制
Dojo Grid 在獲取 Item 時有一種機制叫做“lazy loading”。在 Grid 初始化時並不把 Store 里的所有數據都加載進來, 而是采用“on-demand”的方式進行數據加載。觸發數據加載的事件是 Grid 滾動條的的拖拉動作。當滾動條被拖拉到某一個特定位置時, Grid 會計算出當前滾動條的位置,並把和當前位置相關的數據加載進來。數據加載是按照“頁”為單位裝載的。有兩個比較重要的屬性:
keepRows: 75 // Number of rows to keep in the rendering cache rowsPerPage: 25 //Number of rows to render at a time, and the rows number in each page
在步驟 1 的最后,Grid 使用 render( ) 函數來取回表格、表頭和視圖,並把滾動條停留在 Grid 最頂端。因此,在 Grid 完成一次 render 后加載進來的數據只有 25 條。其余的數據要在用戶拖動滾動條后觸發再按需加載進來。
這樣的數據加載方式,固然是減小了內存開銷,提高了頁面加載速度。但如果此時使用 Grid 的 getItem:(idx) 函數,程序會因為 getItem 函數中的 var data = this._by_idx[idx]; 語句而報錯。因為輸入函數 idx 可能大於當前 Grid 加載進來的數據量,此時通過 idx 去 Grid 中索引 Item 就會導致數組越界報錯。也就是說在“lazy loading”的情況下,數據沒有同時全部載入,這時如果企圖通過 Grid.getItem(idx) 來操作 Store 中的所有數據的修改、刪除是不安全的。
解決這一問題的方法是直接對 Store 中的數據進行獲取、修改、刪除。Store 中的 _arrayOfTopLevelItems 存儲着的 Store 當前數據。因為這些數據和 Grid 的顯示數據是一一對應,所以可以通過參數(Grid Item 的行數)把 Grid 的數據映射到 Store 中,直接對 Store 里的源數據進行操作。
清單 8 中展示的是如何通過直接存取 Store 中的數據來實現對 Grid 數據的操作。程序的第一段定義了 GetItemfromStore 函數,他有兩個參數一個是 Store 的名稱和需要索引的行數。有了這兩個參數,就可以通過 Store._arrayOfTopLevelItems[idx] 獲得對應的存儲在 Store 中 Item 了。
修改 Item 比較簡單,在函數 ModifyItem 中只要由 GetItemfromStore 得到 Item,然后對 Item 修改 setValue 就可以更改 Item 了。刪除 Item 也同樣是由 GetItemfromStore 得到 Item,然后取出 Item 中的某個屬性判斷該 Item 是否符合刪除條件,如果符合就在 Store 中加以刪除。結合 step1 中的操作 Store 前斷開連接,操作完 Store 后恢復連接,就可以實現 Grid 的刪除功能。
清單 8. 對 Store 中存儲的 Item 直接操作
// 獲得 Store 中的 Item function GetItemfromStore(Store,idx) { var Item=eval(Store._arrayOfTopLevelItems[idx]); return Item; } // 修改 Item function ModifyItem() { for (var i=0;i<100;i++){ var Item=GetItemfromStore(modelStore,i); modelStore.setValue(Item,'Loop',i); } } // 刪除 Item function DeleteItem() { var deletnum=0; var pushidx=new Array; modelGrid._setStore(null);// 斷開連接 for(var i=0;i<modelGrid.rowCount;i++){ var Item; Item=GetItemfromStore(modelStore,i);// 獲得 Item if(Item !=null){ var sel = modelStore.getValue(Item,'Sel');// 獲得 Sel 屬性 if(sel==true){ deletnum=deletnum+1; pushidx.push(Item);// 把符合條件的 Itempush 到 Array 中去 } } } var Items = pushidx; /*Store 循環刪除 Item*/ if(Items.length){ for(var i=0;i<Items.length;i++){ modelStore.deleteItem(Items[i]); } } modelGrid._setStore(modelStore);// 恢復連接 modelGrid._refresh();//Grid 更新視圖 }
Step3. 重構 Grid 的排序方法
Grid 具有排序功能,點擊表頭可以實現對表格內容升序或者降序排列。每次排序的操作都是針對 Grid 加載進來的數據進行的。 排序后,當需要對數據操作時,還是先通過 Grid.getItem(idx) 獲得 Item,然后依靠 Item 特有的 identifier 索引到 Store 中的真實數據, 再對數據進行修改。
然而因為“lazy loading”的存在,Grid 並沒有把所有數據同時加載進來,這就導致了 Grid 排序后會出現數據獲得錯誤和數據索引錯誤。 所以為了保證數據索引正確,就需要從數據源上對數據進行排序,這樣才能保證 Grid 和 Store 中的數據順序保持一致。
具體做法是先禁用 Grid 的默認 sort 方法:canSort="false"。然后重新定義 Grid.onHeaderCellMouseDown 的響應函數, 重構 sort 函數,以及更新 Grid 標題視圖。這樣就可以保證 Grid 的排序功能正常運行。
清單 9 展示了重新定義 onHeaderCellMouseDown 的響應函數的過程。函數定義了一個數組來存放臨時變量,並且記錄了表頭上是否存着“全選”。 接着函數尋找記錄需要排序的項目,接着設置此次排序是順序還是逆序排列。設置完成后,就調用自定義的 sort 函數對數據進行排序。排序完后 對表頭進行一定的修改,增加一個向上或者向下的箭頭來標識當前表格的某列是按升序還是降序排列,便於用戶識別。
清單 9. 重新定義 onHeaderCellMouseDown 的響應函數
//Grid.onHeaderCellMouseDown 事件就是鼠標點擊表頭所觸發的事件。 // 我們所要做的就是把這一事件的處理函數重定向到我們自己定義的排序方法。 modelGrid.onHeaderCellMouseDown = function(e){ modelGrid._setStore(null); var instancesArr = new Array(); // 定義一個數組存放排序臨時數據 var allselRrd=dojo.byId('checkcollection').checked;// 記錄表頭上的“全選”狀態 columnSort=e.cellIndex; var propSort=modelGridLayout[0].cells[e.cellIndex].name; // 記錄排序的項目 if(columnSort!=0){ sortAscending=!sortAscending; // 設置正向排序還是逆向排序 for(var i=0;i <modelStore._arrayOfTopLevelItems.length;i++){ instancesArr.push(modelStore._arrayOfTopLevelItems[i]); } sortmodelGrid(instancesArr,propSort); // 重寫 sort 函數 modelStore._arrayOfTopLevelItems=instancesArr; modelGrid._setStore(modelStore); UpdateHeaderView(); // 更新表頭 } dojo.byId('checkcollection').checked=allselRrd; }
清單 10 是我們根據需要重寫的排序函數。在這里主要是對數據做了一個分類處理。如果排序數據是數字的話,就按照大小排列。如果排序數據是子母的話,就先把他們轉換成小寫子母,然后再根據子母順序進行排序。
清單 10. 重構 Sort 函數
// 根據所在列的內容的屬性定制適合的 Sort 函數 function sortmodelGrid(arr,propSorter) { var comp=1; var asc=1; if(sortAscending){ asc=1;} else{ asc=-1;} for(var i=0;i < (arr.length);i++){ for(var j=0;j <(arr.length-1-i);j++){ var aProp=eval("arr[j]."+propSorter+"[0]"); var bProp = eval("arr[j+1]."+propSorter+"[0]"); if(IsNumber(aProp)&& IsNumber(bProp)){ // 如果是數字就直接排序 } else{ // 如果是子母就先轉換成小寫再排序 aProp= aProp.toLowerCase(); bProp = bProp.toLowerCase(); } if(aProp > bProp){ comp=1;} else if(aProp < bProp){ comp=-1;} else{ comp=0;} if((comp*asc) >0){ var Itemm=arr[j+1]; arr[j+1]=arr[j]; arr[j]=Itemm; } } } }
清單 11 所做的就是在 Grid 的表頭欄上增加一個向上或者向下的箭頭來標識當前表格的某列是按升序還是降序排列,便於用戶識別。操作的方法主要是通過根據一些 html 屬性取出表頭的各列的值,對其 html 語言進行修改,插入一個箭頭的符號。
清單 11. 更新 Grid 表頭視圖
// 增加一個向上或者向下的箭頭來標識當前表格的某列是按升序還是降序排列,便於用戶識別 function UpdateHeaderView(){ var docnObj=document.getElementsByTagName("th"); for(var i=0;i < 5;i++){ var docnObjName=modelGridLayout[0].cells[i].name; var ret = [ '<div class="dojoxGridSortNode' ]; if(i==columnSort){ // 通過判斷 sortAscending 是 true 還是 false 來認知當前是升序還是降序排列 // 根據排列順序來修改表頭的 css ret = ret.concat([' ',(sortAscending ==true)?'dojoxGridSortUp':'dojoxGridSortDown','"> <div class="dojoxGridArrowButtonChar">',(sortAscending ==true)? '▲':'▼', '</div ><div class="dojoxGridArrowButtonNode" ></div >' ]); ret = ret.concat([propSort, '</div >']); } else{ ret.push('">'); ret = ret.concat([docnObjName, '</div >']); } docnObj[i].innerHTML=ret.join(" "); } }
圖 4 是 Grid 排序后的一張效果圖。可以看到 Grid 中的各項已經在 Device 列上降序排列了。Device 表頭上多了一個向下的箭頭,表示數據降序排列。
圖 4.Grid 排序效果圖

Grid 性能改善的前后對比
從下面的對比圖中可以看到,經過改造后的 Grid,它在數據處理性能上的提高是非常巨大的。 在 firefox 3.5 上測試,增加 142 個項目的時間由原來的接近 1 分鍾的時間縮短到 1 秒以內。說明這種提高性能的方式是行之有效的。
圖 5. 未進行性能優化耗費的時間圖


圖 7 優化后添加 / 刪除耗費的時間圖

小結
本文解釋了 Dojo Grid 控件為何在數據處理上速度較慢的原因。並且在此基礎上,提供了一種提高 Dojo Grid 處理數據速度的方法, 包括如何適時與 Store 斷開或建立連接,如何為適應“lazy loading”特性更改獲取數據方式以及如何重構排序函數。經過實驗證明,該方法性能良好。
