Jquery.dataTable表格控件源代码重点难点分析

jquery datatable源代码结构:

入口函数(构造函数):
var DataTable = function ( options ){
    // this = $()对象
    this.api = function ( traditional )  // 给$()对象增加属性,api返回插件实例
        {
            return traditional ?
                new _Api(
                    _fnSettingsFromNode( this[ _ext.iApiIndex ] )  // 从DataTable.settings取tbody数据
                ) :
                new _Api( this );
        };
    
    this.each(function() {  // 一般来说页面就一个table容器,就一个实例,但源代码是按多实例写的,所以用了each。
        var oInit =   // 传入的tbody数据保存在oInit
        var oSettings = // oInit保存到oSettings
        var allSettings = DataTable.settings;
        allSettings.push( oSettings );  // oSettings保存到allSettings->DataTable.settings,是$()对象内置函数dataTable的一个属性,从$()也能看到
    })
    
    return this;  // 返回jquery对象$()增加了方法等属性
}

// jQuery access
$.fn.dataTable = DataTable; // 外部用$().dataTable()方式调用,传入的tbody数据保存在DataTable的属性中(settings)


调插件时传入的tbody数据,插件是如何保存的?如何看到这个数据?:
var oTable = $(table-id).dataTable(options);  // 用oTable接收返回的实例
console.log(oTable)

$(table-id).dataTable(options); // 可以不接收返回的实例,插件入口初始化函数会把实例保存到$(table-id).dataTable。
console.log($(table-id).dataTable.settings) 
console.log($(table-id))

// 以上均能看到tbody数据,插件代码把数据保存到jquery对象中,具体来说是保存到$().dataTable对象中。

代码整体结构:
(function(factory){
        factory();
    }(function(){
        var dataTable = function(options){
            
       } 
       dataTable.settings = {};
       $.fn.dataTable = dataTable;
    })  // 匿名函数里面直接引用全局变量$,无需(function($){}($))传参
    );

    $(table-id).dataTable(options); 

它是定义一个dataTable函数(类)存储在jquery对象里面,以便通过jquery对象调用,在dataTable对象里面定义了一个settings对象属性,
做为存储数据的大本营。

简化写法其实就是如下:

    config();

    $(table-id).dataTable(options); 

    function config(){
       var dataTable = function(options){
            // this就是$()
       } 
       dataTable.settings = {};
       $.fn.dataTable = dataTable;
    };
 


当用$(table-id).dataTable(options)多次反复调用插件时,插件要判断是否传参,是否第一次运行,是否要执行初始化table代码,
是否返回实例,其代码是:

var bRetrieve = oInit.bRetrieve !== undefined ? oInit.bRetrieve : defaults.bRetrieve;
                    var bDestroy = oInit.bDestroy !== undefined ? oInit.bDestroy : defaults.bDestroy;
            
                    if ( emptyInit || bRetrieve )
                    {
                        return s.oInstance; // 返回实例
                    }
                    else if ( bDestroy )
                    {
                        s.oInstance.fnDestroy();  // 删除实例
                        break;
                    }
                    else
                    {
                        _fnLog( s, 0, 'Cannot reinitialise DataTable', 3 ); // 报错不再处理
                                                return;
                    }

如果上述检查不通过,就返回不处理了,如果上述检查通过了,才继续往下执行初始化table的代码。

做一个测试,重复调两次插件:
$(table-id).dataTable(options);
$(table-id).dataTable(options);
        
第一次执行时DataTable.settings为空。
第一次执行时保存实例:oSettings.oInstance = (_that.length===1) ? _that : $this.dataTable();
第一次执行调_fnInitialise()根据传入的数据或者用传入的ajax获取的数据构造table插入页面。

第二次执行时bRetrieve/bDestroy = false,输出错误结束代码执行,如果传参为空,emptyInit=true,返回settings里面的oInstance($()对象)。
第二次传不同的参数也一样报错。


datatable布局:
var sDom = "t<'row-fluid' <'iconfont icon-shaixuan sxBox'>"+searchbox+"<'exchange-rate'><p><i><'datatable-fy iconfont icon-tingchechang' l>>"

插件解析sDom字符串构造html:
    function _fnAddOptionsHtml ( oSettings )
    {
    
        // All DataTables are wrapped in a div
        var insert = $('<div/>', {
            id:      oSettings.sTableId+'_wrapper',
            'class': classes.sWrapper + (oSettings.nTFoot ? '' : ' '+classes.sNoFooter)
        } );

        var aDom = oSettings.sDom.split('');
        var featureNode, cOption, nNewNode, cNext, sAttr, j;
        for ( var i=0 ; i<aDom.length ; i++ )
        {
            cOption = aDom[i];
    
            if ( cOption == '<' )  // 循环到<就创建一个newNode
            {
                /* New container div */
                nNewNode = $('<div/>')[0];
                /* Check to see if we should append an id and/or a class name to the container */
                cNext = aDom[i+1];
                if ( cNext == "'" || cNext == '"' )  // 解析"xxx" (class)
                {
                    sAttr = "";  <"wrapper"
                    j = 2;
                    while ( aDom[i+j] != cNext )
                    {
                        sAttr += aDom[i+j];
                        j++;
                    }
                    ...
                    else if ( sAttr.charAt(0) == "#" )
                    {
                        nNewNode.id = sAttr.substr(1, sAttr.length-1);
                    }
                    else
                    {
                        nNewNode.className = sAttr;
                    }
    
                    i += j; /* Move along the position array */
                }
                insert.append( nNewNode );  // 第一次创建newNode插入顶级容器
                insert = $(nNewNode);  // insert变成newNode,下一次嵌套newNode插入上一级newNode
            }
            else if ( cOption == '>' )
            {
                /* End container div */
                insert = insert.parent();  // 遇到>把newNode插入上一级newNode
            }
            // @todo Move options into their own plugins?
            else if ( cOption == 'l' && features.bPaginate && features.bLengthChange )
            {
                /* Length */
                featureNode = _fnFeatureHtmlLength( oSettings );
            }
            else if ( cOption == 'f' && features.bFilter )
            {
                /* Filter */
                featureNode = _fnFeatureHtmlFilter( oSettings );
            }
            else if ( cOption == 'r' && features.bProcessing )
            {
                /* pRocessing */
                featureNode = _fnFeatureHtmlProcessing( oSettings );
            }
            else if ( cOption == 't' )
            {
                /* Table */
                featureNode = _fnFeatureHtmlTable( oSettings );
            }
            else if ( cOption ==  'i' && features.bInfo )
            {
                /* Info */
                featureNode = _fnFeatureHtmlInfo( oSettings );
            }
            else if ( cOption == 'p' && features.bPaginate )
            {
                /* Pagination */
                featureNode = _fnFeatureHtmlPaginate( oSettings );
            }
            else if ( DataTable.ext.feature.length !== 0 )
            {
                /* Plug-in features */
                var aoFeatures = DataTable.ext.feature;
                for ( var k=0, kLen=aoFeatures.length ; k<kLen ; k++ )
                {
                    if ( cOption == aoFeatures[k].cFeature )
                    {
                        featureNode = aoFeatures[k].fnInit( oSettings );
                        break;
                    }
                }
            }
            /* Add to the 2D features array */
            if ( featureNode )
            {
                var aanFeatures = oSettings.aanFeatures;
    
                if ( ! aanFeatures[cOption] )
                {
                    aanFeatures[cOption] = [];
                }
                aanFeatures[cOption].push( featureNode );
                insert.append( featureNode );
            }
        }
    
        /* Built our DOM structure - replace the holding div with what we want */
        holding.replaceWith( insert );
        oSettings.nHolding = null;
    }


这个插件有个重要功能就是排序,table点击表头可以按列数据重新排序,排序处理函数_fnSort,用arr.sort(fn)对tr数组重新排序,写一个比较函数即可:
displayMaster.sort( function ( a, b ) { 

}
displayMaster是一个排序数组:
displayMaster = $(table-id).dataTable.settings[0].aiDisplayMaster);
其初始值是[0,1,2,3,...],数组index就是页面tr的index,值是tr在原始数据中对应的index,重新排序之后,比如倒序排序之后,此数组变为[...3,2,1,0],
最后一行tr在原始数据中的index=0没变,只是在这个数组的位置变成了最后一个,按这个数组循环构造tr,原来第一个tr就在最后一个。

点击重新排序是对这个数组重新排序,排序是用index找列数据值比较数据值的大小,并不是比较index的大小,然后根据这个数组重新构造table/tr,相当于tbodyList的顺序变了,但数据没变,只是重构页面tr。

// 点击表头th事件绑定:
function _fnInitialise ( settings ){
    _fnBuildHead( settings );

function _fnBuildHead( oSettings ){
    _fnSortAttachListener( oSettings, column.nTh, i );

function _fnSortAttachListener ( settings, attachTo, colIdx, callback ){
    _fnBindAction( attachTo, {}, function (e) {
        _fnSortListener( settings, colIdx, e.shiftKey, callback );

function _fnBindAction( n, oData, fn ){
        $(n)
            .on( 'click.DT', oData, function (e) {
                    n.blur(); // Remove focus outline for mouse users
                    fn(e); // fn是传入的匿名函数,会调_fnSortListener
                } )
    
function _fnSortListener ( settings, colIdx, append, callback ){
    // Run the sort by calling a full redraw
        _fnReDraw( settings );
        
        
function _fnReDraw( settings, holdPosition ){
    _fnSort( settings );

function _fnSort ( oSettings ){
    displayMaster.sort( function ( a, b ) {

    }
}

它底层就是用$().on()绑定事件,只是层层封装复杂,插件绑定事件的代码很难找到,因为它封装得复杂。
前端是数据呈现,界面和交互,事件绑定是最重要的代码,一般不会轻易让你找出来。

事件可以写namespace,在解除绑定时可以用namespace解除具有相同namespace的事件,效率高,但
绑定事件时namespace无效,比如click.xxx,没有click.xxx事件,除非插件自己触发产生click.xxx逻辑事件。

$(document).on("click.xxx", function(){  // 点击有效
    console.log('click')
})
$(document).off(".xxx");  // 解除绑定有效


有一个函数能检测浏览器:
function _fnBrowserDetect( settings )

它是构造元素插入页面,再读取属性,从而判断浏览器是否支持某种属性。


fixed-columns.js是实现固定列,比如左边第一列,当水平方向滚动table时,第一列固定不动。
它其实是把第一列复制一份,浮动定位在第一列显示,把第一列盖住,造成第一列没有滚动的假象。
固定列有个问题,当第一列是checkbox需要点击选取时,由于table真正的第一列被遮挡,就没法点击了。


差几案代码还涉及到Hungarian变量命名法:
m_lpszStr
iMyData

其实变量命名是任意的,只要符合js的规定即可,压缩代码全是abcd没有换行,是最不规范的代码,除了效率之外,
就是成心让你没法看。


这个插件考虑到ajax分页获取数据问题,应用可以写一个ajax函数传递给插件,由插件代为执行:    
options.ajax = function(data, callback, settings){
    准备参数...
    调接口获取数据...
    调插件的callback(传获取的table数据)

插件调应用的ajax函数时会传callback,应用处理完最后调callback继续执行插件的代码,应用ajax函数代码与插件无关,
如何调接口获取数据是应用代码的事情,插件不涉及这个事情,插件只管得到table数据构造table。

插件有关ajax和分页的处理代码如下:
分页块构造代码:
function _fnFeatureHtmlPaginate ( settings ){

代码在DT_bootstrap.js中:
"fnInit": function( oSettings, nPaging, fnDraw ) {


分页按钮构造以及绑定点击事件:
"fnUpdate": function ( oSettings, fnDraw ) {
    for ( j=iStart ; j<=iEnd ; j++ ) {
                    sClass = (j==oPaging.iPage+1) ? 'class="active"' : '';
                    $('<li '+sClass+'><a href="#">'+j+'</a></li>')
                        .insertBefore( $('li:last', an[i])[0] )
                        .bind('click', function (e) {
                            e.preventDefault();
                            oSettings._iDisplayStart = (parseInt($('a', this).text(),10)-1) * oPaging.iLength;
                            fnDraw( oSettings );
                        } );

function _fnAjaxUpdate( settings ){

    _fnBuildAjax(
                settings,
                _fnAjaxParameters( settings ),
                function(json) {
                    _fnAjaxUpdateDraw( settings, json );
                }
            );
            
function _fnBuildAjax( oSettings, data, fn ){
    var ajax = oSettings.ajax;
        var instance = oSettings.oInstance;
        var callback = function ( json ) {
            _fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] );
            fn( json );
        };
    oSettings.jqXHR = ajax.call( instance, data, callback, oSettings );


ajax调完之后执行_fnAjaxUpdateDraw( settings, json ) -> _fnDraw( settings ) 更新table数据和tr


其实构造table很简单,比较复杂的是分页调ajax,重新排序,构造td时可以调事先写好的回调函数,用回调函数
来构造td,这个功能要写成通用的插件形式有点复杂,比如可以根据后台返回的theadList数据决定调用哪个事先写好
的回调函数自动构造td的内容为input或者checkbox。

参数以a/b开头是旧版本写法,可以兼容。

  • 17
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值