使用jQueryUI的widget來寫插件,相比於基本的jquery插件有一些好處:
* 方便實現繼承,代碼重用
* 默認是單例
* widget已經給你實現好的一些常用方法,例如destroy
帶來好處的同時也帶來了荊棘和陷阱,本文的目的就是梳理這些荊棘,標出哪里有陷阱。
基本知識:命名規范,public, private, this, this.element
如何開始寫一個widget呢?模板如下:
(function ($) { // utility functions (won’t be inherited) function foo() {} $.widget('命名空間.插件名', $.繼承插件的命名空間.插件名,{ /* snip */ }); })(jQuery);
其中命名空間是可選的,要不要繼承別的widget也是可選的。大頭是后面snip的部分,這也是下文要講的。
一般來說工具函數寫在widget外面比較合適,但如果你想要這些工具函數被子類繼承,則需要寫在widget里面。
寫在widget里面的,就有public和private之分,規則是:
public方法首字符不是_
private方法首字符是_
當調用方法時,會先判斷是否以_開頭,如果是則不執行調用。
如果我非要在外面調用private方法,該怎么做?並非一點辦法也沒有:
var instance = $('<div>'); instance.mywidget('publicFunction'); // work instance.mywidget('_privateFunction'); // silently fail instance.data('mywidget')._privateFunction(); // work $.mynamespace.mywidget.prototype._privateFunction(); // work
在widget內,this表示的是什么?我們在widget的一個public函數內用console.log(this)打出來瞧瞧:
日志顯示,this是一個$.widget.$.(anonymous function).(anonymous function)
this.element是變成widget的那個jQuery對象,如果要用jquery的方法,往往首先要取到jquery對象。
this.options是插件的選項,下文會詳解。
this.__proto__包含了插件中定義的所有public和private函數,以及繼承過來的方法。
這里簡單介紹一下__proto__:每個對象都會在其內部初始化一個屬性,就是__proto__,當我們訪問一個對象的屬性 時,如果這個對象內部不存在這個屬性,那么他就會去__proto__里找這個屬性,這個__proto__又會有自己的__proto__,於是就這樣 一直找下去,也就是我們平時所說的原型鏈的概念。
_create _init destroy
widget factory實現了一種單例模式,即不允許在同一個jQuery對象上多次實例化。
當調用$(XX).widgetName()進行初始化的時候,會執行以下代碼(源碼截取自jquery.ui.widget.js):
var instance = $.data( this, name ); // 從widget自身取出名字為name的數據 if ( instance ) { instance.option( options || {} )._init(); // 若該數據已經存在則只調用_init } else { $.data( this, name, new object( options, this ) ); // 若數據還沒有則新建一個實例,並將實例保存 }
當調用$(XX).widgetName(‘destroy’)進行銷毀的時候,執行以下代碼(源碼截取自jquery.ui.widget.js):
this.element .unbind( "." + this.widgetName ) .removeData( this.widgetName ); // 刪除在create時保存的數據
有一個removeData的操作,那么下次調用$(XX).widgetName()就會重新實例化了。
需要注意的是,destroy方法在jquery.ui.widget.js中是有默認實現的,而_create和_init沒有實現。因此如果用自己的方法覆蓋destroy,不要忘記調用默認的:
destory: function () { console.log('destory'); // call the original destroy method since we overwrote it $.Widget.prototype.destroy.call(this); }
以下示例代碼驗證_create和_init的區別以及destroy的作用:
var mw = $('#test').myWidget(); // _create _init mw = $('#test').myWidget(); // _init mw.myWidget('destory'); mw = $('#test').myWidget(); // _create _init
那么在_create和_init以及destroy里分別應該做什么:
_create: 生成HTML,事件綁定。
_init: 執行默認的初始化動作,例如把頁面變成初始狀態。
destory: 調用$.Widget.prototype.destroy.call(this),刪除HTML。
注意:綁定事件要注意給事件名加命名空間后綴:例如 .bind('mouseenter.mywidget', this._hover)
options
選項,在widget中的定義是options,而在調用時是option,注意定義的時候有s,調用的時候沒s。
定義:
option
s
: { field1: 'default', function1: function () { console.log('default option function1'); } },
調用:
$('#test').mywidget('option', 'field1', 2);
widget默認實現了兩個函數:_setOptions和_setOption,_setOptions的實現就是對每個要修改的option調用_setOption,也就是說真正修改的動作在_setOption里。因此,如果要重寫_setOption函數,則一定不要忘記寫:
$.Widget.prototype._setOption.apply(this, arguments);
_setOptions和_setOption這倆函數什么時候被調用呢?用下面這個例子來說明。
例如有這樣的_setOption和_setOptions:
_setOption: function (key, value) { console.log('_setOption: key=%s value=%s', key, value); $.Widget.prototype._setOption.apply(this, arguments); }, _setOptions: function (options) { var key; console.group('_setOptions'); for (key in options) { this._setOption(key, options[key]); } console.groupEnd(); return this; },
以及一個打印options值的函數printOptions:
printOptions: function () { console.group('options'); console.log('field1: %s', this.options.field1); console.log('function1: %s', this.options.function1); console.groupEnd(); },
我們像下面這樣調用:
var instance = $('<div>'); // create widget with default options console.group(); instance.mywidget(); instance.mywidget('printOptions'); console.groupEnd(); // create widget with specified options instance.mywidget('destroy'); console.group(); var opts = { field1: 'specified', function1: function () { console.log('specified option function1'); }, }; instance.mywidget(opts); instance.mywidget('printOptions'); console.log('-------------'); instance.mywidget(opts); console.groupEnd(); // modify options console.group(); instance.mywidget('option', 'field1', 2); instance.mywidget('printOptions'); console.groupEnd();
打出的日志如下:
日志分為三大塊。
第一塊是不使用options來初始化,可以看到直接使用定義里默認的options,是不調用_setOption的。
第二塊是使用options來初始化,這一塊做了兩個實驗(日志中用--------將兩塊分隔),第一個實驗是完全重建(_create, _init),從日志可以看到並沒有調用_setOption;第二個實驗只是重新初始化(_init),用的options都一樣,從日志可以看到它調用了_setOption,且在_init之前調用的。
第三塊不是初始化,而僅僅是修改option值,可以清楚看到調用了_setOption。
何時會調用_setOption的結論:
1. 像instance.mywidget('option', 'field1', 2); 這樣顯式設置option時。
2. 帶着options初始化時:
如果實例不存在,即需要調用_create,則不調用_setOption;
如果實例已存在,僅需要調用_init,則會在調用_init之前調用_setOption。
_trigger
注意這個_trigger是jQueryUI widget factory里的,和jQuery里$.fn命名空間下的trigger函數不是一個東西(后者不帶下划線)。
_trigger一般用來回調用戶傳入options的callback。
在插件內部調用_trigger(‘myEvent’)即相當於調用options里面的myEvent這個回調函數。
要改動options里的event handler應該怎么做呢?不要使用bind/unbind,而是去修改options:
// bind (overwrite, not add event handler) mw.myWidget('option', 'myEvent', function (event, ui) { console.log('new implement'); }); // unbind mw.myWidget('option', 'myEvent', null);
總結一下:
this._trigger(‘eventName’)是widget特有的,用於調用options里定義的callback。
this.element.trigger(‘eventName’)是jQuery的,可參考jQuery的事件的用法。(其中this.element是表示該插件的jQuery對象)
一個_trigger的樣例:
// 模板
this._trigger( "callbackName" , [eventObject], [uiObject] )
callbackName
The name of the event you want to dispatcheventObject
(Optional)An (mocked) event object. _trigger
wraps this object and stores it in event.originalEvent
The user receives an object with event.type == this.widgetEventPrefix + "eventname"
uiObject
(Optional)An object containing useful properties the user may need to access.Protip: Use a method like ._ui
to generate objects with a consistent schema.
// 調用樣例 this._trigger( "hover", e /* e.type == "mouseenter" */, { hovered: $(e.target)}); // The user can subscribe using an init option $("#elem").filterable( { hover: function(e,ui) { } } ); // Or with traditional event binding/delegation $("#elem").bind( "filterablehover" , function(e,ui) { } );