• jQuery EasyUI Datagrid VirtualScrollView视图简单分析


      大家都知道EasyUI的Datagrid组件在加载大数据量时的优势并不是很明显,相对于其他一些框架,如果数据量达到几千,便会比较慢,特别是在IE下面。针对这种情况,我们首要做的是要相办法优化datagrid组件的各方面性能,不过任何事情都是可以变通解决的,virtualScrollView就是一种不错的解决方案。

    virtualScrollView的准则就是尽量少画tr到table里,表格的高度是有限的,而用户的可见区域是很有限的,所以数据量很大的时候,是没有必要将所有数据数据都画到表格中,这样造成庞大的DOM,导致加载速度变慢。

    源码分析

    jQuery EasyUI的datagrid组件官方也扩展了一个virtualScrollView视图,我们来分析一下它的源码:

     
    1. var scrollview = $.extend({}, $.fn.datagrid.defaults.view, {   
    2.     render: function(target, container, frozen){   
    3.         var state = $.data(target, 'datagrid');   
    4.         var opts = state.options;   
    5.         //这个地方要特别注意,并不是用的state.data.rows数据   
    6.         //而是用的view.rows,而view.rows在onBeforeRender事件中被设置为undefined了   
    7.         //onBeforeRender事件在scrollview中,即便是url方式有也只会被触发一次,所以在第一次rend时,是没有数据直接return了。   
    8.         var rows = this.rows || [];   
    9.         if (!rows.length) {   
    10.             return;   
    11.         }   
    12.         var fields = $(target).datagrid('getColumnFields', frozen);   
    13.            
    14.         //如果是rend frozen部分,但是有没有行号和frozenColumns的话,那就直接返回   
    15.         if (frozen){   
    16.             if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))){   
    17.                 return;   
    18.             }   
    19.         }   
    20.            
    21.         var index = this.index;   
    22.         var table = ['<table class="datagrid-btable" cellspacing="0" cellpadding="0" border="0"><tbody>'];   
    23.         for(var i=0; i<rows.length; i++) {   
    24.             var css = opts.rowStyler ? opts.rowStyler.call(target, index, rows[i]) : '';   
    25.             var classValue = '';   
    26.             var styleValue = '';   
    27.             if (typeof css == 'string'){   
    28.                 styleValue = css;   
    29.             } else if (css){   
    30.                 classValue = css['class'] || '';   
    31.                 styleValue = css['style'] || '';   
    32.             }   
    33.             var cls = 'class="datagrid-row ' + (index % 2 && opts.striped ? 'datagrid-row-alt ' : ' ') + classValue + '"';   
    34.             var style = styleValue ? 'style="' + styleValue + '"' : '';   
    35.             // get the class and style attributes for this row   
    36. //          var cls = (index % 2 && opts.striped) ? 'class="datagrid-row datagrid-row-alt"' : 'class="datagrid-row"';   
    37. //          var styleValue = opts.rowStyler ? opts.rowStyler.call(target, index, rows[i]) : '';   
    38. //          var style = styleValue ? 'style="' + styleValue + '"' : '';   
    39.             var rowId = state.rowIdPrefix + '-' + (frozen?1:2) + '-' + index;   
    40.             table.push('<tr id="' + rowId + '" datagrid-row-index="' + index + '" ' + cls + ' ' + style + '>');   
    41.             table.push(this.renderRow.call(this, target, fields, frozen, index, rows[i]));   
    42.             table.push('</tr>');   
    43.             index++;   
    44.         }   
    45.         table.push('</tbody></table>');   
    46.            
    47.         $(container).html(table.join(''));   
    48.     },   
    49.        
    50.     /**
    51.      * onBeforeRender事件,首先要明白两点:  
    52.      * 1-调用loadData方法加载数据数据时,loadData内部rend之前会触发这个事件  
    53.      * 2-url方式时,获取到远程数据之后,也是使用loadData方法加载数据的,所以url方式也会触发onBeforeRender事件  
    54.      * @param  {DOM} target datagrid实例的宿主DOM对象  
    55.      * @return {[type]}        [description]  
    56.      */  
    57.     onBeforeRender: function(target){   
    58.         var state = $.data(target, 'datagrid');   
    59.         var opts = state.options;   
    60.         var dc = state.dc;   
    61.         var view = this;   
    62.         // 删除onLoadSuccess事件,防止被触发,将备份到state.onLoadSuccess上   
    63.         state.onLoadSuccess = opts.onLoadSuccess;   
    64.         opts.onLoadSuccess = function(){};   
    65.            
    66.         opts.finder.getRow = function(t, p){   
    67.             var index = (typeof p == 'object') ? p.attr('datagrid-row-index') : p;   
    68.             var row = $.data(t, 'datagrid').data.rows[index];   
    69.             if (!row){//什么情况会取不到呢?   
    70.                 var v = $(t).datagrid('options').view;   
    71.                 row = v.rows[index - v.index];   
    72.             }   
    73.             return row;   
    74.         };   
    75.            
    76.         dc.body1.add(dc.body2).empty();   
    77.         this.rows = undefined;  // 把需要画的tr绑定到view.rows上了   
    78.         this.r1 = this.r2 = []; // view.r1和viwe.r2分别存放对第一页tr和最后一页tr的引用   
    79.         //这里不要想当然,只是绑定了事件,在第一次加载数据时,究竟是什么时候触发这个事件的呢   
    80.         //这个问题得追溯到loadData方法了,每次loadData之后都会直接使用triggerHandler触发scroll的   
    81.         dc.body2.unbind('.datagrid').bind('scroll.datagrid', function(e){   
    82.             if (state.onLoadSuccess){   
    83.                 opts.onLoadSuccess = state.onLoadSuccess;   // 恢复onLoadSuccess事件   
    84.                 state.onLoadSuccess = undefined;   
    85.             }   
    86.             if (view.scrollTimer){// 清除定时器   
    87.                 clearTimeout(view.scrollTimer);   
    88.             }   
    89.             // 延时五十毫秒执行   
    90.             view.scrollTimer = setTimeout(function(){   
    91.                 scrolling.call(view);   
    92.             }, 50);   
    93.         });   
    94.            
    95.         function scrolling(){   
    96.             if (dc.body2.is(':empty')){//dc.body2对应普通列数据,如果为空的话,说明没有数据。   
    97.                 //没有数据就尝试加载数据   
    98.                 reload.call(this);   
    99.             } else {   
    100.                 var firstTr = opts.finder.getTr(target, this.index, 'body', 2);   
    101.                 var lastTr = opts.finder.getTr(target, 0, 'last', 2);   
    102.                 var headerHeight = dc.view2.children('div.datagrid-header').outerHeight();   
    103.                 var top = firstTr.position().top - headerHeight;   
    104.                 var bottom = lastTr.position().top + lastTr.outerHeight() - headerHeight;   
    105.                    
    106.                 if (top > dc.body2.height() || bottom < 0){   
    107.                     reload.call(this);   
    108.                 } else if (top > 0){   
    109.                     var page = Math.floor(this.index/opts.pageSize);   
    110.                     this.getRows.call(this, target, page, function(rows){   
    111.                         this.r2 = this.r1;   
    112.                         this.r1 = rows;   
    113.                         this.index = (page-1)*opts.pageSize;   
    114.                         this.rows = this.r1.concat(this.r2);   
    115.                         this.populate.call(this, target);   
    116.                     });   
    117.                 } else if (bottom < dc.body2.height()){// 需要加载下一页的情况   
    118.                     var page = Math.floor(this.index/opts.pageSize)+2;   
    119.                     if (this.r2.length){   
    120.                         page++;   
    121.                     }   
    122.                     this.getRows.call(this, target, page, function(rows){   
    123.                         if (!this.r2.length){   
    124.                             this.r2 = rows;   
    125.                         } else {   
    126.                             this.r1 = this.r2;   
    127.                             this.r2 = rows;   
    128.                             this.index += opts.pageSize;   
    129.                         }   
    130.                         this.rows = this.r1.concat(this.r2);   
    131.                         this.populate.call(this, target);   
    132.                     });   
    133.                 }   
    134.             }   
    135.                
    136.             function reload(){   
    137.                 var top = $(dc.body2).scrollTop();//被卷起的高度   
    138.                 var index = Math.floor(top/25);//获取被卷起的行索引,如:卷起一行半37.5,index为1   
    139.                 var page = Math.floor(index/opts.pageSize) + 1;//获取页数,如果每页10条,卷起262.5,page为2   
    140.                    
    141.                 this.getRows.call(this, target, page, function(rows){   
    142.                     this.index = (page-1)*opts.pageSize;//view.index存放的是page页第一行的索引   
    143.                     this.rows = rows;//view.rows存放需要画的tr   
    144.                     this.r1 = rows;   
    145.                     this.r2 = [];   
    146.                     this.populate.call(this, target);   
    147.                     dc.body2.triggerHandler('scroll.datagrid');   
    148.                 });   
    149.             }   
    150.         }   
    151.     },   
    152.        
    153.     getRows: function(target, page, callback){   
    154.         var state = $.data(target, 'datagrid');   
    155.         var opts = state.options;   
    156.         var index = (page-1)*opts.pageSize;   
    157.         var rows = state.data.rows.slice(index, index+opts.pageSize);   
    158.         if (rows.length){//这是一次性加载完所有数据的方式,可以直接从本地javascript数组中取出数据   
    159.             callback.call(this, rows);   
    160.                
    161.         } else {//懒加载方式   
    162.             var param = $.extend({}, opts.queryParams, {   
    163.                 page: page,   
    164.                 rows: opts.pageSize   
    165.             });   
    166.             if (opts.sortName){   
    167.                 $.extend(param, {   
    168.                     sort: opts.sortName,   
    169.                     order: opts.sortOrder   
    170.                 });   
    171.             }   
    172.             if (opts.onBeforeLoad.call(target, param) == falsereturn;   
    173.                
    174.             $(target).datagrid('loading');   
    175.             var result = opts.loader.call(target, param, function(data){   
    176.                 $(target).datagrid('loaded');   
    177.                 var data = opts.loadFilter.call(target, data);   
    178.                 callback.call(opts.view, data.rows);   
    179. //              opts.onLoadSuccess.call(target, data);   
    180.             }, function(){   
    181.                 $(target).datagrid('loaded');   
    182.                 opts.onLoadError.apply(target, arguments);   
    183.             });   
    184.             if (result == false){   
    185.                 $(target).datagrid('loaded');   
    186.             }   
    187.         }   
    188.     },   
    189.        
    190.     populate: function(target){   
    191.         var state = $.data(target, 'datagrid');   
    192.         var opts = state.options;   
    193.         var dc = state.dc;   
    194.         var rowHeight = 25;   
    195.            
    196.         if (this.rows.length){   
    197.             opts.view.render.call(opts.view, target, dc.body2, false);   
    198.             opts.view.render.call(opts.view, target, dc.body1, true);   
    199.             // 看到了么,滚动条有那么大空间是怎么实现的了么?用的padding!   
    200.             dc.body1.add(dc.body2).children('table.datagrid-btable').css({   
    201.                 paddingTop: this.index*rowHeight,   
    202.                 paddingBottom: state.data.total*rowHeight - this.rows.length*rowHeight - this.index*rowHeight   
    203.             });   
    204.             opts.onLoadSuccess.call(target, {   
    205.                 total: state.data.total,   
    206.                 rows: this.rows   
    207.             });   
    208.         }   
    209.     }   
    210. });   

    分析结论

    • virtualScrollView原理是通过设置div的上下padding来达到模拟极大数据量的效果的,我们只画比可视部分多一点的tr
    • EasyUI的virtualScrollView支持两种方式:一是一次性请求完所有数据;二是每次都是ajax到pageSize条数据
    • EasyUI的virtualScrollView画的tr数量是2*pageSize(初次加载例外,这时候只画1*pageSize的tr)
    • EasyUI的virtualScrollView视图把行高强制视为25px的,如果你设置非25px的行高,这个视图就不能正常工作
    • 因为只画2*pageSize个tr,所以我们dategrid的高度不能设置得超过2*25*pageSize个像素,超过的话就会造成可视区有留白
    • 使用loadData方法加载数据的话loadData入参不需要total属性,只要是rows数组就可以了,total在loadData内部会自动计算

    对于前面几点,大家自己看看源码里我写的注释,基础差的,看个似懂非懂就行了,基础好的,最好就彻底研究下。

    存在的Bug

    请求后台死循环

    如果是url方式,第一次加载不到数据,就会不断地请求后台。看到146行了么,如果回调函数没有接受到rows,是不应该触发scorll事件的,因为scroll事件会请求后台数据,我已我们只要加上条件就行了:

     
    1. if(rows && rows.length > 0){   
    2.     dc.body2.triggerHandler('scroll.datagrid');   
    3. }  
    二次请求后台

    url方式下,如果后台返回数据不足以填充表格高度的时候,会重复请求后台(注意这地方只重复请求一次,跟第一个bug不同)。这个问题的原因也很简单,其实这种情况,datagrid高度有点大,但是后台又只有很少几条数据造成的,表现在只有一批数据,而这批数据又不足以填满这个表格可视区高度。我们把122行对getRows方法的调用加个条件就可以了:

     
    1. if (this.rows.length == opts.pageSize) {   
    2.     this.getRows.call(this, target, page, function(rows) {   
    3.         if (!this.r2.length) {   
    4.             this.r2 = rows;   
    5.         } else {   
    6.             this.r1 = this.r2;   
    7.             this.r2 = rows;   
    8.             this.index += opts.pageSize;   
    9.         }   
    10.         this.rows = this.r1.concat(this.r2);   
    11.         this.populate.call(this, target);   
    12.     });   
    13. }  

    this.rows是当前已经画的一批rows,如果rows的条数没有pageSize大,那就说明不需要再请求数据了。

    virtualScrollView是一种很好的优化手段,以后会被应用的越来越广的,EasyUI的VirtualScrollView视图是否支持editor我并有去尝试,估计是不支持的,有兴趣的同学可以去研究研究。

     
    *****转载:http://www.easyui.info/archives/1404.html
     
  • 相关阅读:
    C#绘制矢量图
    VC6配置boost
    MapX特性分析
    BCG文档
    VC单实例运行
    MapInfo7.0序列号和许可文件
    Mapx的VC开发实践
    网页设计实训
    illustrator初学之绘图基础1
    Ai绘图基础2
  • 原文地址:https://www.cnblogs.com/linybo/p/10054051.html
Copyright © 2020-2023  润新知