dojo
当您要在网页上显示层次结构数据时,Dojo v1.4 TreeGrid
是一个有用的小部件。 但是,如果您的数据集很大, TreeGrid
的性能将非常慢。 在本文中,学习如何通过自定义TreeGrid
和QueryStore
解决此问题。 本文介绍了使用两个小部件时可能遇到的问题,解释了错误的原因,然后帮助您创建解决方案。 您还可以下载本文中使用的示例代码。
Dojo网格和大数据集
当然,您可以使用网格在浏览器中有效显示相对较小的数据集。 您将获得排序,列大小调整等优点。 但是,在任何给定时间可以处理的记录数实际上都有限制,最终会导致分页结果的问题。
您可能会忘记分页功能,这些日子终于过去了。 Dojo的网格在网格滚动时使用惰性文档对象模型(DOM)渲染。 对于具有少于几百条记录的相对较小的数据集,在滚动时,惰性DOM呈现将为预加载的数据集构建DOM。 例如,如果您有100条记录,但一次只能查看20条记录,则无需滚动到21至100中的任何记录,直到滚动到该特定部分。 按列排序和相关任务可以在小型数据集中按预期方式在内存中工作; 您可以有效地用JavaScript完成任务。
对于非常大的数据集,将JavaScript用于按列排序之类的任务很快变得不切实际,甚至是不可能的。 如果数据集很大,尝试使用ItemFileReadStore
在浏览器中维护它是不切实际的,该ItemFileReadStore
会将所有数据加载到浏览器中。
Dojo处理大型数据集的方法简单,优雅,可归结为两点:
- 一个dojo.data实现,可以从服务器以任意页面大小请求数据。 本文使用
QueryReadStore
。 - 一个能够滚动的网格,它可以按需请求和加载特定页面。
本文的其余部分将向您展示如何使用QueryReadStore
构建延迟加载的树网格。
QueryReadStore,TreeGrid和TreeModel
本节对QueryReadStore
, TreeGrid
和TreeModel
了非常基本的介绍。
QueryReadStore
正如dojocampus.org DOC(见说明相关信息 ), QueryReadStore
是非常相似的ItemReadStore
。 他们都使用JSON作为交换格式。 不同之处在于他们查询数据的方式。 ItemReadStore
从服务器进行一次获取,并处理客户端中的所有排序和筛选。 几百条记录就可以了。 但是对于成千上万的记录或Internet连接速度缓慢, ItemReadStore
不太可行。
QueryReadStore
对服务器进行每次排序或查询的请求,使其非常适合具有小数据窗口的大型数据集,例如dojox.grid.DataGrid
。 清单1显示了如何创建QueryReadStore
。
清单1.创建一个QueryReadStore
var url = "./servlet/QueryStoreServlet";
var store = new dojox.data.QueryReadStore({
url: url,
requestMethod: "get"
});
TreeGrid和TreeModel
树小部件(例如Tree
和TreeGrid
)提供了分层数据的视图。 TreeGrid
小部件本身仅是数据视图。 真正的强大功能来自TreeModel
,它表示Tree
小部件将显示的实际分层数据。
通常,数据最终来自数据存储,但是Tree
小部件与dijit.tree.Model
(与树所需的特定API匹配的对象)连接。 因此,“ Tree
窗口小部件可以访问各种格式的数据,例如使用数据存储项访问其父项。
TreeModel
负责某些任务,例如连接到数据源,延迟加载以及从Tree
小部件中查询项目和项目的层次结构。 例如,一项任务可能是为“ Tree
窗口小部件获取子项。 TreeModel
还负责将数据更改通知给Tree
。
首先,您需要定义ForestStoreModel
的查询( TreeModel
的实现)以返回TreeGrid
的多个顶级项。 您还将为树创建模型适配器以访问商店。 您需要清单2中的参数来构造TreeModel
。
清单2.创建TreeModel的代码
var query = {
type: "PARENT"
};
var treeModel = new dijit.tree.ForestStoreModel({
store: store, // the data store that this model connects to
query: query, // filter multiple top level items
rootId: "$root$",
rootLabel: "ROOT",
childrenAttrs: ["children"], // children attributes used in data store.
/*
For efficiency reasons, Tree doesn't want to query for the children
of an item until it needs to display them. It doesn't want to query
for children just to see if it should draw an expando (+) icon or not.
So we set "deferItemLoadingUntilExpand" to true.
*/
deferItemLoadingUntilExpand: true
});
QueryReadStore,TreeGrid和TreeModel之间的关系
要了解如何使用TreeGrid
,请记住以下相互配合的树组件:
-
QueryReadStore
负责从服务器获取数据。 -
ForestTreeModel
负责从QueryReadStore
查询项目的层次结构,并通知TreeGrid
更新。 -
TreeGrid
仅负责显示数据和处理用户事件。
图1显示了这三个之间的关系。 清单3显示了如何创建树。
图1. QueryReadStore,TreeGrid和TreeModel之间的关系

清单3.创建一棵树
// define the column layout for the tree grid.
var layout = [
{ name: "Name", field: "name", width: "20%" },
{ name: "Age", field: "age", width: "auto" },
{ name: "Position", field: "position", width: "auto" },
{ name: "Telephone", field: "telephone", width: "auto" }
];
// tree grid
var treegrid = new dojox.grid.TreeGrid({
treeModel: treeModel,
structure: layout, // define columns layout
/*
A 0-based index of the cell in which to place the actual expando (+)
icon. Here we define the "Name" column as the expando column.
*/
expandoCell: 0,
defaultOpen: false,
columnReordering: true,
rowsPerPage: 20,
sortChildItems: true,
canSort: function(sortInfo) {
return true;
}
}, "treegrid");
treegrid.startup();
QueryReadStore如何与TreeGrid一起使用
本节探讨如何在TreeGrid
渲染数据,使用QueryReadStore
排序以及扩展父节点。
渲染图
QueryReadStore
将向服务器发送顶级节点的请求。 存储对服务器的第一个请求将是对顶级节点(根的子代)的请求。 此dojo.data请求遵循特定的JSON格式。
树状网格将使用提供给模型的查询来发出初始请求,然后商店会将其与目标结合起来。 清单4显示了请求格式,REST URL模式以及服务器对请求的响应。 在示例中,请求是针对REST模式的。
清单4.查询请求和服务器响应
// query
{
query: {type: "PARENT"},
start: 0,
count: 20 //rowsPerPage attribute of the tree grid
}
// request sample
url?type=PARENT&start=0&count=20
// server response
{
"identifier": "id",
"label": "name",
"items": [
{ "id": "id_0",
"name": "Edith Barney",
"age": 39,
"position": "Marketing Manager",
"telephone": 69000044
"children": true,
"type": "PARENT"
},
{ "id": "id_1",
"name": "Herbert Jeames",
"age": 43,
"position": "Brand Executive Manager",
"telephone": 69000077,
"type": "Child"
},
...
],
"numRows": 10000 // total records in the grid
}
项目"Edith Barney"
的类型为"PARENT"
, "Herbert Jeames"
项目的类型为"Child"
。 树模型中的查询为{type: "PARENT"},
因此树网格最初将仅显示"Edith Barney"
记录。
您实际上不需要包括孩子; children
属性的存在将向Tree
指示该节点是可扩展的,并且将包含一个扩展图标。 项目"Edith Barney"
的children
属性不是一个假值,因此树模型会将其视为带有孩子的节点。 树形网格中的Edith Barney行中将有一个加号(+)。 图2显示了结果。
图2.创建的TreeGrid的渲染结果
现在,您已经在TreeGrid
呈现了数据,现在您将要验证TreeGrid
功能(如排序和扩展父节点)是否可以与QueryReadStore
一起正常使用。
排序
要执行排序功能,请单击“名称”列的标题,然后按名称升序排序。 以升序或降序对其他列进行排序是相同的。
清单5.排序
// the tree model will pass a request to the query store like this:
{
query: {type: "PARENT"},
start: 0,
count: 20,
sort: {
attribute: "name",
descending: false
}
}
// The query store will request server like this:
url?type=PARENT&start=0&count=20&sort=name
图3显示了排序结果。
图3.排序结果
扩展父节点
对于大型树数据集,您可能只想为树的可见节点加载必要的数据。 当用户扩展节点时,它将开始加载该节点的子级。 理想情况下,您只想针对每个扩展发出一个HTTP请求即可获得最佳性能。
当用户单击示例中的节点之一时,树将要求商店加载项目,并且商店将请求资源。 但是可能会发生奇怪的事情,例如在我们的例子中,加号图标消失而子行未显示。
图4.扩展无法正常工作
那么,为什么它不起作用?
查看清单6中的TreeGrid
和TreeModel
的源代码,以了解扩展子行的工作方式。
清单6. TreeGrid和TreeModel如何扩展子行
//TreeGrid.setOpen
setOpen: function(open){
...
treeModel.getChildren(itm, function(items){
d._loadedChildren = true;
d._setOpen(open);
});
...
}
//TreeModel.getChilren
getChildren: function(/*dojo.data.Item*/ parentItem, /*function(items)*/ onComplete,
/*function*/ onError){
// summary:
// Calls onComplete() with array of child items of given parent item, all loaded.
var store = this.store;
if(!store.isItemLoaded(parentItem)){
// The parent is not loaded yet, we must be in deferItemLoadingUntilExpand mode,
// so we will load it and just return the children (without loading each child item)
var getChildren = dojo.hitch(this, arguments.callee);
store.loadItem({
item: parentItem,
onItem: function(parentItem){
getChildren(parentItem, onComplete, onError);
},
onError: onError
});
return;
}
// get children of specified item
var childItems = [];
for(var i=0; i<this.childrenAttrs.length; i++){
var vals = store.getValues(parentItem, this.childrenAttrs[i]);
childItems = childItems.concat(vals);
}
}
树模型将要求商店首先加载父项。 父项未完全加载,因为其children
属性不是列出该项所有子级的数组。
但是,这种情况将以某种方式通过store.isItemLoaded(parentItem)
检查逻辑,并且您有childItems=[true]
。 由于true不是有效的项目,因此树状网格将跳过此无效的子项目,这不会导致任何事情发生。 查看清单7中store.isItemLoaded
方法的代码,以了解发生了什么。
清单7. store.isItemLoaded
isItemLoaded: function(/* anything */ something){
// Currently we don't have any state that tells if an item is loaded or not
// if the item exists it's also loaded.
// This might change when we start working with refs inside items ...
return this.isItem(something);
}
此方法中的注释提供了解释。 QueryReadStore
目前不支持部分加载项目。
自定义QueryReadStore以支持部分加载
在本节中,您将自定义QueryReadStore
和TreeGrid
以实现部分加载并修复回归错误。
您可以下载所有自定义的代码。 这是一个在Eclipse中运行的Web项目,易于配置。
创建CustomQueryStore
若要使树形网格的扩展功能与QueryReadStore
一起QueryReadStore
,必须自定义QueryReadStore
以使其与部分加载的项目一起使用。 首先,您必须扩展该类以添加缺少的方法。 如清单8所示,只需将CustomQueryStore
子类QueryReadStore
到CustomQueryStore
。
清单8.子类QueryReadStore
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\data\CustomQueryStore.js */
dojo.provide("demo.data.CustomQueryStore");
dojo.require("dojox.data.QueryReadStore");
dojo.declare("demo.data.CustomQueryStore", dojox.data.QueryReadStore, {
/* @Override */
isItemLoaded: function(/* anything */ something) {
// TODO
}
});
下一步是更改检查项目是否已加载的规则,如清单9所示。
清单9. isItemLoaded方法
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\grid\CustomTreeGrid.js */
/* @Override isItemLoaded method */
isItemLoaded: function(/* anything */ something) {
// Currently we have item["children"] as a state that tells if an item is
// loaded or not.
// if item["children"] === true, means the item is not loaded.
var isLoaded = false;
if (this.isItem(something)) {
var children = this.getValue(something, "children");
if (children === true) {
// need to lazy loading children
isLoaded = false;
} else {
isLoaded = true;
}
}
return isLoaded;
}
QueryReadStore
没有loadItem
方法,因此您将创建它。 此方法将向服务器请求一个列出该项目所有子项的数组。
清单10.覆盖loadItem,getValues和setValues
/* treegrid-demo\WebContent\script\dojo-1.4.3\demo\grid\CustomTreeGrid.js */
/* @Override loadItem method */
loadItem: function(/* object */ args) {
if (this.isItemLoaded(args.item)) {
return;
}
var item = args.item;
var scope = args.scope || dojo.global;
var sort = args.sort || null;
var onItem = args.onItem;
var onError = args.onError;
if (dojo.isArray(item)) {
item = item[0];
}
// load children
var children = this.getValue(item, "children");
// load children
if (children === true) {
var serverQuery = {};
// "parent" param
var itemId = this.getValue(item, "id");
serverQuery["parent"] = itemId;
// "sort" param
if (sort) {
var attribute = sort.attribute;
var descending = sort.descending;
serverQuery["sort"] = (descending ? "-" : "") + attribute;
}
// ajax request
var _self = this;
var xhrData = {
url: this.url,
handleAs: "json",
content: serverQuery
};
var xhrFunc = (this.requestMethod.toLowerCase() === "post") ?
dojo.xhrPost : dojo.xhrGet;
var deferred = xhrFunc(xhrData);
// onError callback
deferred.addErrback(function(error) {
if (args.onError) {
args.onError.call(scope, error);
}
});
// onLoad callback
deferred.addCallback(function(data) {
if (!data) {
return;
}
if (dojo.isArray(data)) {
var children = data;
var parentItemId = itemId;
var childItems = [];
dojo.forEach(children, function(childData) {
// build child item
var childItem = {};
childItem.i = childData;
childItem.r = this;
childItems.push(childItem);
}, _self);
_self.setValue(item, "children", childItems);
}
if (args.onItem) {
args.onItem.call(scope, item);
}
});
}
}
/* @Override geValues method */
getValues: function(item, attribute) {
// summary:
// See dojo.data.api.Read.getValues()
this._assertIsItem(item);
if (this.hasAttribute(item, attribute)) {
return item.i[attribute] || [];
}
return []; // Array
}
/* @Override seValue method */
setValue: function(/* item */ item, /* attribute-name-string */ attribute,
/* almost anything */ value) {
// summary: See dojo.data.api.Write.set()
// Check for valid arguments
this._assertIsItem(item);
this._assert(dojo.isString(attribute));
this._assert(typeof value !== "undefined");
var success = false;
var _item = item.i;
_item[attribute] = value;
success = true;
return success; // boolean
}
现在,该示例具有一个新的CustomQueryStore
。 替换JavaScript中的QueryReadStore
,如清单11所示。
清单11.使用CustomQueryStore发送请求
var url = "./servlet/QueryStoreServlet";
var store = new demo.data.CustomReadStore({
url: url,
requestMethod: "get"
});
该代码返回图5所示的结果。
图5. customQueryStore返回的结果
自定义TreeGrid修复回归错误
现在该检查其他功能,以确保在应用CustomQueryStore
之后没有回归错误。
首先展开一行,然后单击“名称”标题以执行排序。 如图6所示,发生错误。
图6.对CustomQueryStore进行排序时发生错误
在调查之后,您发现树状网格将始终保持expando
函数的状态。 在执行排序操作之前,将展开第四行,并且树形网格会记住它。 排序后,项目有所不同,但树状网格仍将尝试打开第四行的expando
功能。 发生错误,因为现在第四行的项目没有任何子代。 当树状网格执行排序查询时,您需要清除expando
状态。
就像您自定义QueryReadStore
,您也需要自定义TreeGrid
。 然后,使用CustomTreeGrid
替换TreeGrid
应该可以解决排序缺陷。
清单12.自定义TreeGrid
dojo.provide("demo.grid.CustomTreeGrid");
dojo.require("dojox.grid.TreeGrid");
dojo.declare("demo.grid.CustomTreeGrid", dojox.grid.TreeGrid, {
/* @Override */
sort: function() {
this.closeExpando();
this.inherited(arguments);
},
closeExpando: function(identities) {
if (identities) {
if (dojo.isArray(identities)) {
// close multiple expando
dojo.forEach(identities, function(identity) {
this._closeExpando(identity);
}, this);
} else {
// close single expando
var identity = identities;
this._closeExpando(identity);
}
} else {
// close all expando
var expandoCell = this.getCell(this.expandoCell);
for (var identity in expandoCell.openStates) {
this._closeExpando(identity);
}
}
},
_closeExpando: function(identity) {
var expandoCell = this.getCell(this.expandoCell);
if (expandoCell.openStates.hasOwnProperty(identity) === true) {
var open = expandoCell.openStates[identity] || false;
if (open === true) {
// clean up expando cache
this._cleanupExpandoCache(null, identity, null);
}
}
}
});
在一起, TreeGrid
和QueryReadStore
是延迟加载数据的强大组合。 无需大型的前期数据传输,就可以显示大型的,广泛的分层数据。 您可以利用QueryReadStore
的部分加载支持来对每个扩展使用单个请求执行延迟加载。
性能比较
在本文中,我们比较了使用ItemFileReadStore
和QueryReadStore
的TreeGrid
性能。 图7和表1显示,定制的QueryStore
和网格仅使用ItemFileReadStore
和基本TreeGrid
所用时间的约1/30。
图7. ItemFileReadStore和CustomQueryStore之间的性能比较

表1.性能比较
二手数据存储 | 服务器数据加载时间(秒) | 网格发布时间(秒) | 总 |
---|---|---|---|
ItemFileReadStore | 1.45 | 12.65 | 14.1 |
CustomQueryStore | 0.14 | 0.37 | 0.51 |
摘要
在本文中,您学习了如何创建自定义解决方案来提高Dojo TreeGrid
加载大型数据的性能。 您探索了QueryReadStore
, TreeModel
和TreeGrid
如何QueryReadStore
工作以获取和呈现数据。 定制QueryStore
和TreeGrid
可以QueryStore
解决Dojo当前不支持大数据的问题。 文章中的结果表明,该解决方案的性能大约是ItemFileReadStore
和TreeGrid
用时间的1/30。
翻译自: https://www.ibm.com/developerworks/web/library/wa-dojotreegrid/index.html
dojo