内容


提升 Dojo TreeGrid 性能

以较少的时间加载大量数据

Comments

简介

如果您想要在一个 web 页面上呈现分层数据,Dojo TreeGrid 是一很有用的小部件。 但是如果您有一个大数据集,那么 TreeGrid 速度将极其缓慢。在本文中学习如何通过定制 TreeGridQueryStore 缓解这一问题。本文描述了您在使用这两个小部件时可能遇到的各种问题,解释了这些错误形成的原因,然后帮助您创建一个解决方案。您可以 下载 本文所用的样例代码。

Dojo 网格和大数据集

当然,您可以使用网格在浏览器中有效地显示相对较小的数据集。您可以获得精确的排序、列大小调整等等。然而,实际上对您在一定时间内可以处理的记录数量是有限制的,这最终将导致页编码问题。

您可以忘记分页了 — 那些日子已经一去不返了。Dojo 网格使用延迟加载 DOM 渲染(lazy DOM rendering)作为网格卷轴。对于一个只有几百条记录的相对较小的数据集,当您滚动时懒惰 DOM 渲染为预加载的数据集增建 DOM。例如,如果您有 100 条记录,但是每次只能查看 20 条,您不需要为第 20 至 100 条记录构建节点,除非您滚动到某一条。按一列或者相关任务进行排序在内存中像小数据集那样运行;您使用 JavaScript 就可以有效地完成任务。

对于非常庞大的数据集,您很快就会发现使用 JavaScript 执行任务(比如按某列进行排序)将变得不切实际,甚至不可行。如果数据集非常庞大,试图在浏览器中使用一个 ItemFileReadStore 对其进行维护是不切实际的,因为这需要将所有数据下载到浏览器中。

Dojo 处理大数据集的方法简洁精致,可归结为以下两个方面:

  • 一个 dojo.data 实现可以以任意页面大小从服务器请求数据。本文使用 QueryReadStore
  • 一个有滚动能力的网格,它可以根据需要请求和加载一个特定的页面。

本文余下部分将向您介绍如何使用 QueryReadStore 构建一个延迟加载(lazy-loaded)树型网格。

QueryReadStore、TreeGrid 和 TreeModel

本小节提供了 QueryReadStoreTreeGridTreeModel 的基础介绍。

QueryReadStore

正如 dojocampus.org doc 文档所述(见 参考资料),QueryReadStoreItemReadStore 非常相似。它们都是用 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

一个树型小部件,比如 TreeTreeGrid,显示一个分层数据视图。TreeGrid 小部件本身只不过是该数据的一个视图,真正的力量来自于 TreeModel,它呈现 Tree 小部件将要显示的实际分层数据。

通常,数据最终来自一个数据存储库,但 Tree 小部件与一个 dijit.tree.Model(匹配此树所需的方法的某个 API 的一个对象 )连接。因此,Tree 小部件可以以各种格式访问数据,比如一个数据存储,其中条目引用它们的父节点。

TreeModel 负责某些任务,比如连接到数据源、延迟加载、以及从 Tree 小部件查询条目和条目层次结构。例如,一个任务也能获取这个 Tree 小部件的一个子条目。TreeModel 还负责将 Tree 的更改通知给该数据。

开始之前,您需要定义 ForestStoreModelTreeModel 的一个实现)的查询来返回 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 个组件之间的关系。清单 3 显示了如何创建一个树。

图 1. QueryReadStore、TreeGrid 和 TreeModel 之间的关系
Flowcart 显示从服务器到用户活动的步骤,queryreadstore、foresttreemodel 和 treegrid 作为步骤
Flowcart 显示从服务器到用户活动的步骤,queryreadstore、foresttreemodel 和 treegrid 作为步骤
清单 3. 创建一个 Tree
// 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 将发送面向顶级节点的请求到服务器,Store 向服务器发送的第一个请求将是面向顶级节点(根节点的子节点)的一个请求。这个 dojo.data 请求遵循一个特定的 JSON 格式。

树型网格将使用提供给模型的查询发出初始请求,Store 将它和目标结合起来。清单 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 正常运作。

排序

要试用排序功能,单击 Name 列标题,根据名称升序排列。其他列的升序或降序排序也相同。

清单 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 中的 TreeGridTreeModel 源代码,看看如何展开子条目。

清单 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]。因为 ture 不是一个有效条目。树型网格将跳过这个无效的字条目,这就导致什么都没出现。查看清单 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 以支持部分加载

在这一小节,您将定制 QueryReadStoreTreeGrid 来实现部分加载和修复回归 bug。

您可以 下载 定制的所有代码。这是一个易于配置的 web 项目,在 Eclipse 中运行。

创建 CustomQueryStore

要同时使用树型网格的展开功能和 QueryReadStore,您必须定制 QueryReadStore 使其能支持一个部分加载的条目。首先,您必须扩展这个类来添加缺少的方法,如清单 8 所示,只需将子类 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 来修复回归 bug

是时候核查其他特性了,确保应用 CustomQueryStore 之后就没有回归 bug 了。

首先,展开一行,然后单击 Name 标题执行排序。如图 6 所示,出现一个错误。

图 6. CustomQueryStore 排序之后出现错误
第一行上出现‘sorry an error occurred’ 的屏幕截图
第一行上出现‘sorry an error occurred’ 的屏幕截图

调查之后,您发现树型网格总是保留 expando 函数的状态。排序之前,第 4 行是展开的,树型网格记住了这个状态。排序之后,条目有所不同,但是属性网格仍然想要打开第 4 行的 expando 功能。错误就出现了,因为现在第 4 行的条目没有子条目。当树型网格执行一个排序查询时,您需要清除 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);
            }
        }
    }

});

合起来,TreeGridQueryReadStore 是一个功能强大的处理延迟加载数据的组合。不需要大量前期数据转换就可显示规模庞大的分层数据。您可以利用 QueryReadStore 的部分加载支持对每次展开用一个请求执行延迟加载

性能比较

在本文中,我们比较使用 ItemFileReadStore 和使用 QueryReadStoreTreeGrid 的性能。图 7 和表 1 显示了定制的 QueryStore,网格使用的仅是使用 ItemFileReadStore 和基本 TreeGrid 时的 1/30 时长。

图 7. ItemFileReadStore 和 CustomQueryStore 的性能比较
显示 itemfilereadstore 读取时间为 1400,而 queryreadstore 显示少于 100 的条形图。
显示 itemfilereadstore 读取时间为 1400,而 queryreadstore 显示少于 100 的条形图。
表 1. 性能比较
使用的数据存储库 服务器加载时间(秒) 网格渲染时间(秒) 总计
ItemFileReadStore 1.45 12.65 14.1
CustomQueryStore 0.14 0.37 0.51

结束语

在本文中,您学习了如何创建一个定制的解决方案来提升 Dojo 加载大数据的性能。您也学习了 QueryReadStoreTreeModelTreeGrid 如何共同合作来获取和渲染大数据。定制 QueryStoreTreeGrid 可以解决目前 Dojo 不能很好地支持大数据这一问题。本文结果显示解决方案的性能大约是 ItemFileReadStoreTreeGrid 所用时间的 1/30。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=776946
ArticleTitle=提升 Dojo TreeGrid 性能
publish-date=11282011