2016年4月27日 星期三

jQuery Widget Factory - Part 2

Widget Factory - Part 2

簡介

這篇談 widget 的生命週期 (lifecycle)。在繼承了 $.widget 之後,有幾個繼承而來的基本函式是一定要好好實作的,否則宣告的 widget 就很難有所作為,或者在使用時也會造成很多的不便和困擾。這篇談的就是這些非實作不可的基本函式;但並未真正實作出函式,只放了一些簡單的程式碼做為研究之用;而是先討論在整個 widget 的生命週期中,這些函式是在何時被呼叫、執行及真正要實作時,大概哪些作業要放在哪個函式中實作。

this.options

在談到必要的基本函式之前,一定要先提到一個很重要的屬性 - options,這個屬性的值是一個 Javascript 的物件,用來儲存這個 widget 在運作時所要使用到的屬性及屬性的預設值,還有當指定事件被觸發時,所要執行的回呼函式 (callback)。實際上,從 $.widget 中就繼承了一個 options 的物件,這個物件中只有 disabled 及 create 兩個屬性,在這個 widget 中所宣告和定義的 options,最終會和 $.widget 中所繼承過來的 options 合併 (merge) 成一個物件。若是繼承其它的模組,最終也會和該模組的 options 物件合併成一個物件。在整個 widget 的實作過程中,都以 this.options 來參考這個合併過後的物件。而下述的兩個函式: _setOptions() 及 _setOption() 也是針對 this.options 這個物件的屬性及屬性值在作業。

Basic Methods

widget 生命週期中一定會執行到的基本函式如下,這裡先簡單的介紹各函式的主要作用及執行時機,在範例的程式碼中,可以更清楚的看出各函式的執行時機。這些函式中,通常都會在函式的某處呼叫 this._super() 或 this._superApply() 以執行父物件中同名的函式,確保父物件中同名的函式也會被執行。

_create()
這個函式只有在 widget 第一次被呼叫,將 widget 具體化 (instantiate) 時會執行。函式中首先要做的是在 DOM tree 中構建 widget 所需要的各個節點,及將要監聽的事件和該事件的處理器綁定 (bind) 在一起。因為只會被執行一次,所以在整個 widget 的生命週期中,只能做一次和只需做一次的作業,都放到這個函式裡。具體化時,執行完 _create() 之後,會緊接著執行 _init()。一般 _create() 可以不傳入參數,但也可以傳入一個 Javascript 的物件,物件的內容可以用來修改 this.oprions 的內容。
_init()
jquery-ui 中將建立 (create) 和初始化 (initialize) 視為兩個概念,因此分成兩個階段來執行。_init() 除了在第一次具體化時跟在 _create() 後面被執行一次之外,每次呼叫 widget 但不傳入任何參數時,例如: $(selector).widget(); _init() 也都會被執行一次。此時,會先執行 _setOptions() 之後再執行 _init()。和 _create() 一樣, _init() 一般也不傳入參數,但也可以傳入一個 Javascript 的物件,物件的內容也是用來修改 this.oprions 的內容。注意:_init() 在整個 widget 的生命週期中,可能被執行不止一次,這點和 _create() 不同,因此,整個整個生命週期中只能執行一次的作業絕對不能放在這個函式裡。
_setOption(key, value)
將 this.options 之名為 key 之屬性的值改成 value。這個函式實際上不會被外部程式直接呼叫,它都是透過 _setOptions() 函式的呼叫才會執行。如果某個 this.options 中的屬性值有限制條件,可以在這裡做檢查;或者屬性值變更後,需要觸發某事件等,都在這裡處理。重點是 _setOption() 會實際去改變 this.options 的屬性值,要等到屬性值改變完畢之後才能執行的作業,一定得在這個函式中確定 this.options 中的屬性值被修改完畢後才能進行。
_setOptions(opts)
只要呼叫 $(selector).widget("option", key, value); 或是 $(selector).widget("option", {key1:value1, key2:value2}); ,以變更 this.options 中的屬性值,都會執行 _setOptions() 函式,再由 _setOptions() 為每一個傳入的屬性呼叫一次 _setOptions()。只有呼叫 $(selector).widget("option", key); 以取得 key 屬性的值時,不會執行 _setOptions()。另外,就是在進行被始化時,會在執行 _init() 之前被執行。這個函式大多在進行改變 this.options 屬性值的前置作業。
_destroy()
摧毀此 widget。把 destroy 譯成「摧毀」好像很可怕,但這個函式基本上,就是把 widget 在 DOM tree 中所建構的東西都清除乾淨,讓 DOM tree 回復到尚未建構這個 widget 之前的狀態。 $(selector).widget("destroy"); 呼叫 destroy() 公用函式, destroy() 公用函式則把工作再指派給 _destroy()。所以可以不用理會 destroy() 公用函式 只要實作 _destroy() 函式即可。只是在有繼承父類別時,在清除 DOM tree 中的東西時,得特別注意一下清除的前後次序。如果在 widget 中一次把所有的東西都清光了,有時父類別的 _destroy() 函式會因為找不到東西清除而出現錯誤。

範例

範例在網頁下載完畢後,網頁上會出現一個橘紅色圓圈,這個圓圈其實跟 widget 的生命週期沒什麼關係。只是用來顯示網頁已經下載完畢而己。記得開啟「開發者工具」中的「主控台」看程式執行的結果。範例式程中,每個基本的函式中,都只寫了二行程式碼,第一行將該函式的名稱印在主控台上;第二行呼叫 this._super() 執行父物件中的相對應函式。主程式大略把一個 widget 在生命週期中會被外部程式呼叫的函式,都呼叫了一遍,從主控台中的輸出結果,可以看出每次呼叫時,哪些 widget 的基本函式會依照何種順序執行。

CSS

<style>
html {
    width:100%;
    height:auto;
}
body {
    margin:0 auto;
    width:1200px;
    padding-top:20px;
}
#item {
    width:200px;
    height:200px;
    border-radius: 50%;
    background-color:coral;
}
</style>

CSS 中用 #item{} 顯示了一個用來表示網頁已經下載完畢的橘色圓圈。

HTML

<body>
    
</body>

HTML 中只建立了一個 id="item" 的元件,用來顯示橘色圓圈和掛載 widget。

Script

<script>
// widget created by widget factory
(function($, undefined) {
    $.widget( "prod.item", {
        version:"1.0.0",
        options:{
            product:"Coffee",
            price:80
        },
        _create:function(){
            console.log("\t_create()");
            this._super();
        },
        _init:function(){
            console.log("\t_init()");
            this._super();
        },
        _setOption:function(key, value){
            console.log("\t_setOption()");
            this._super(key, value);
        },
        _setOptions:function(opts){
            console.log("\t_setOptions()");
            this._super(opts);
        },
        _destroy:function(){
            console.log("\t_destroy()");
            this._super();
        }
    });
})(jQuery);

    
$(function(){
    console.log('var elem = $("#item").item()');
    var elem = $("#item").item();

    console.log("elem.item()");
    elem.item();

    console.log('elem.item("option")');
    console.log("\t%o", elem.item("option"));

    console.log('elem.item("option", {product:"Milk", price:50})');
    elem.item("option", {product:"Milk", price:50})
    console.log("\t%o", elem.item("option"));

    console.log('elem.item("option", "price", 60)');
    elem.item("option", "price", 60);

    console.log('elem.item("option", "price")');
    console.log("\t%s", elem.item("option", "price"));

    console.log('elem.item("destroy")');
    elem.item("destroy");
});
</script>

第 5 行,習慣上會加上 version 屬性,並標示 widget 的版本號。
兩段程式都很簡單,一看就懂,不多做說明了。只是從主控台結果裡發現了一件事,要提醒一下:在第 36 行具體化物件時,不論是否有傳入物件參數以改變 this.options 的屬性值,都只會先執行 _create() 接著執行 _init(),是不會執行 _setOptions() 函式的,更不用說 _setOption() 函式了,因此,如果在這兩個函式中,有針對傳入參數的作檢核或是任何前置處理,都不會被執行到。如果在具體化 widget 時,有要傳入物件參數以改變 this.options 之內容的要求,則得在 _create() 函式中自行呼叫 _setOptions() 函式,以完成參數的檢核或前置處理作業。

沒有留言:

張貼留言