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开头是旧版本写法,可以兼容。