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"
οnclick="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”特性更改获取数据方式以及如何重构排序函数。经过实验证明,该方法性能良好。