jquery技巧之讓任何組件都支持類似DOM的事件管理

来源:http://www.cnblogs.com/lyzg/archive/2016/04/05/5352126.html
-Advertisement-
Play Games

本文介紹一個jquery的小技巧,能讓任意組件對象都能支持類似DOM的事件管理,也就是說除了派發事件,添加或刪除事件監聽器,還能支持事件冒泡,阻止事件預設行為等等。在jquery的幫助下,使用這個方法來管理普通對象的事件就跟管理DOM對象的事件一模一樣,雖然在最後當你看到這個小技巧的具體內容時,你可... ...


本文介紹一個jquery的小技巧,能讓任意組件對象都能支持類似DOM的事件管理,也就是說除了派發事件,添加或刪除事件監聽器,還能支持事件冒泡,阻止事件預設行為等等。在jquery的幫助下,使用這個方法來管理普通對象的事件就跟管理DOM對象的事件一模一樣,雖然在最後當你看到這個小技巧的具體內容時,你可能會覺得原來如此或者不過如此,但是我覺得如果能把普通的發佈-訂閱模式的實現改成DOM類似的事件機制,那開發出來的組件一定會有更大的靈活性和擴展性,而且我也是第一次使用這種方法(見識太淺的原因),覺得它的使用價值還蠻大的,所以就把它分享出來了。

在正式介紹這個技巧之前,得先說一下我之前考慮的一種方法,也就是發佈-訂閱模式,看看它能解決什麼問題以及它存在的問題。

1. 發佈-訂閱模式

很多博客包括書本上都說javascript要實現組件的自定義事件的話,可以採用發佈-訂閱模式,起初我也是堅定不移地這麼認為的,於是用jquery的$.Callbacks寫了一個:

define(function(require, exports, module) {

    var $ = require('jquery');
    var Class = require('./class');


    function isFunc(f) {
        return Object.prototype.toString.apply(f) === '[object Function]';
    }

    /**
     * 這個基類可以讓普通的類具備事件驅動的能力
     * 提供類似jq的on off trigger方法,不考慮one方法,也不考慮命名空間
     * 舉例:
     * var e = new EventBase();
     * e.on('load', function(){
     *  console.log('loaded');
     * });
     * e.trigger('load');//loaded
     * e.off('load');
     */
    var EventBase = Class({
        instanceMembers: {
            init: function () {
                this.events = {};
                //把$.Callbacks的flag設置成一個實例屬性,以便子類可以覆蓋
                this.CALLBACKS_FLAG = 'unique';
            },
            on: function (type, callback) {
                type = $.trim(type);
                //如果type或者callback參數無效則不處理
                if (!(type && isFunc(callback))) return;

                var event = this.events[type];
                if (!event) {
                    //定義一個新的jq隊列,且該隊列不能添加重覆的回調
                    event = this.events[type] = $.Callbacks(this.CALLBACKS_FLAG);
                }
                //把callback添加到這個隊列中,這個隊列可以通過type來訪問
                event.add(callback);
            },
            off: function (type, callback) {
                type = $.trim(type);
                if (!type) return;

                var event = this.events[type];
                if (!event) return;

                if (isFunc(callback)) {
                    //如果同時傳遞type跟callback,則將callback從type對應的隊列中移除
                    event.remove(callback);
                } else {
                    //否則就移除整個type對應的隊列
                    delete this.events[type];
                }
            },
            trigger: function () {
                var args = [].slice.apply(arguments),
                    type = args[0];//第一個參數轉為type

                type = $.trim(type);
                if (!type) return;

                var event = this.events[type];
                if (!event) return;

                //用剩下的參數來觸發type對應的回調
                //同時把回調的上下文設置成當前實例
                event.fireWith(this, args.slice(1));
            }
        }
    });

    return EventBase;
});

(基於seajs以及《詳解Javascript的繼承實現》介紹的繼承庫class.js)

只要任何組件繼承這個EventBase,就能繼承它提供的on off trigger方法來完成消息的訂閱,發佈和取消訂閱功能,比如我下麵想要實現的這個FileUploadBaseView:

define(function(require, exports, module) {

    var $ = require('jquery');
    var Class = require('./class');
    var EventBase = require('./eventBase');

    var DEFAULTS = {
        data: [], //要展示的數據列表,列表元素必須是object類型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}]
        sizeLimit: 0, //用來限制BaseView中的展示的元素個數,為0表示不限制
        readonly: false, //用來控制BaseView中的元素是否允許增加和刪除
        onBeforeRender: $.noop, //對應beforeRender事件,在render方法調用前觸發
        onRender: $.noop, //對應render事件,在render方法調用後觸發
        onBeforeAppend: $.noop, //對應beforeAppend事件,在append方法調用前觸發
        onAppend: $.noop, //對應append事件,在append方法調用後觸發
        onBeforeRemove: $.noop, //對應beforeRemove事件,在remove方法調用前觸發
        onRemove: $.noop //對應remove事件,在remove方法調用後觸發
    };

    /**
     * 數據解析,給每個元素的添加一個唯一標識_uuid,方便查找
     */
    function resolveData(ctx, data){
        var time = new Date().getTime();
        return $.map(data, function(d){
            d._uuid = '_uuid' + time + Math.floor(Math.random() * 100000);
        });
    }

    var FileUploadBaseView = Class({
        instanceMembers: {
            init: function (options) {
                this.base();
                this.options = this.getOptions(options);
            },
            getOptions: function(options) {
                return $.extend({}, DEFAULTS, options);
            },
            render: function(){

            },
            append: function(data){

            },
            remove: function(prop){

            }
        },
        extend: EventBase
    });

    return FileUploadBaseView;
});

實際調用測試如下:
無標題
image
測試中,實例化了一個FileUploadBaseView對象f,並設置了它的name屬性,通過on方法添加一個跟hello相關的監聽器,最後通過trigger方法觸發了hello的監聽器,並傳遞了額外的兩個參數,在監聽器內部除了可以通過監聽器的函數參數訪問到trigger傳遞過來的數據,還能通過this訪問f對象。

從目前的結果來說,這個方式看起來還不錯,但是在我想要繼續實現FileUploadBaseView的時候碰到了問題。你看我在設計這個組件的時候那幾個訂閱相關的option:
image 
我原本的設計是:這些訂閱都是成對定義,一對訂閱跟某個實例方法對應,比如帶before的那個訂閱會在相應的實例方法(render)調用前觸發,不帶before的那個訂閱會在相應的實例方法(render)調用後觸發,而且還要求帶before的那個訂閱如果返回false,就不執行相應的實例方法以及後面的訂閱。最後這個設計要求是考慮到在調用組件的實例方法之前,有可能因為一些特殊的原因,必須得取消當前實例方法的調用,比如調用remove方法時有的數據不能remove,那麼就可以在before訂閱裡面做一些校驗,能刪除的返回true,不能刪除的返回false,然後在實例方法中觸發before的訂閱後加一個判斷就可以了,類似下麵的這種做法:

image

但是這個做法只能在單純的回調函數模式里實現,在發佈-訂閱模式下是行不通的,因為回調函數只會跟一個函數引用相關,而發佈-訂閱模式里,同一個消息可能有多個訂閱,如果把這種做法應用到發佈-訂閱裡面,當調用this.trigger('beforeRender')的時候,會把跟beforeRender關聯的所有訂閱全部調用一次,那麼以哪個訂閱的返回值為準呢?也許你會說可以用隊列中的最後一個訂閱的返回值為準,在大多數情況下也許這麼干沒問題,但是當我們把“以隊列最後的一個訂閱返回值作為判斷標準”這個邏輯加入到EventBase中的時候,會出現一個很大的風險,就是外部在使用的時候,一定得清楚地管理好訂閱的順序,一定要把那個跟校驗等一些特殊邏輯相關的訂閱放在最後面才行,而這種跟語法、編譯沒有關係,對編碼順序有要求的開發方式會給軟體帶來比較大的安全隱患,誰能保證任何時候任何場景都能控制好訂閱的順序呢,更何況公司裡面可能還有些後來的新人,壓根不知道你寫的東西還有這樣的限制。

解決這個問題的完美方式,就是像DOM對象的事件那樣,在消息發佈的時候,不是簡簡單單的發佈一個消息字元串,而是把這個消息封裝成一個對象,這個對象會傳遞給它所有的訂閱,哪個訂閱里覺得應該阻止這個消息發佈之後的邏輯,只要調用這個消息的preventDefault()方法,然後在外部發佈完消息後,調用消息的isDefaultPrevented()方法判斷一下即可:
image
而這個做法跟使用jquery管理DOM對象的事件是一樣的思路,比如bootstrap的大部分組件以及我在前面一些博客中寫的組件都是用的這個方法來增加額外的判斷邏輯,比如bootstrap的alert組件在close方法執行的時候有一段這樣的判斷:
image
按照這個思路去改造EventBase是一個解決問題的方法,但是jquery的一個小技巧,能夠讓我們把整個普通對象的事件管理變得更加簡單,下麵就讓我們來瞧一瞧它的廬山真面目。

2. jquery小技巧模式

1)技巧一

如果在定義組件的時候,這個組件是跟DOM對象有關聯的,比如下麵這種形式:
image
那麼我們可以完全給這個組件添加on off trigger one這幾個常用事件管理的方法,然後將這些方法代理到$element的相應方法上: 
無標題2
通過代理,當調用組件的on方法時,其實調用的是$element的on方法,這樣的話這種類型的組件就能支持完美的事件管理了。

2)技巧二

第一個技巧只能適用於跟DOM有關聯的組件,對於那些跟DOM完全沒有關聯的組件該怎麼添加像前面這樣完美的事件管理機制呢?其實方法也很簡單,只是我自己以前真的是沒這麼用過,所以這一次用起來才會覺得特別新鮮: 
無標題
看截圖中框起來的部分,只要給jquery的構造函數傳遞一個空對象,它就會返回一個完美支持事件管理的jquery對象。而且除了事件管理的功能外,由於它是一個jquery對象。所以jquery原型上的所有方法它都能調用,將來要是需要借用jquery其它的跟DOM無關的方法,說不定也能參考這個小技巧來實現。

3. 完美的事件管理實現

考慮到第2部分介紹的2種方式裡面有重覆的邏輯代碼,如果把它們結合起來的話,就可以適用所有的開發組件的場景,也就能達到本文標題和開篇提到的讓任意對象支持事件管理功能的目標了,所以最後結合前面兩個技巧,把EventBase改造如下(是不是夠簡單):

define(function(require, exports, module) {

    var $ = require('jquery');
    var Class = require('./class');

    /**
     * 這個基類可以讓普通的類具備jquery對象的事件管理能力
     */
    var EventBase = Class({
        instanceMembers: {
            init: function (_jqObject) {
                this._jqObject = _jqObject && _jqObject instanceof $ && _jqObject || $({});
            },
            on: function(){
                return $.fn.on.apply(this._jqObject, arguments);
            },
            one: function(){
                return $.fn.one.apply(this._jqObject, arguments);
            },
            off: function(){
                return $.fn.off.apply(this._jqObject, arguments);
            },
            trigger: function(){
                return $.fn.trigger.apply(this._jqObject, arguments);
            }
        }
    });

    return EventBase;
});

實際調用測試如下
1)模擬跟DOM關聯的組件
測試代碼一:

define(function(require, exports, module) {
    var $ = require('jquery');
    var Class = require('mod/class');
    var EventBase = require('mod/eventBase');

    var Demo = window.demo = Class({
        instanceMembers: {
            init: function (element,options) {
                this.$element = $(element);
                this.base(this.$element);

                //添加監聽
                this.on('beforeRender', $.proxy(options.onBeforeRender, this));
                this.on('render', $.proxy(options.onRender, this));
            },
            render: function () {
                //觸發beforeRender事件
                var e = $.Event('beforeRender');
                this.trigger(e);
                if(e.isDefaultPrevented())return;
                //主要邏輯代碼
                console.log('render complete!');
                //觸發render事件
                this.trigger('render');
            }
        },
        extend: EventBase
    });

    var demo = new Demo('#demo', {
        onBeforeRender: function(e) {
            console.log('beforeRender event triggered!');
        },
        onRender: function(e) {
            console.log('render event triggered!');
        }
    });
    
    demo.render();
});

在這個測試里, 我定義了一個跟DOM關聯的Demo組件並繼承了EventBase這個事件管理的類,給beforeRender事件和render事件都添加了一個監聽,render方法中也有列印信息來模擬真實的邏輯,實例化Demo的時候用到了#demo這個DOM元素,最後的測試結果是:
image
完全與預期一致。

測試代碼二:

define(function(require, exports, module) {
    var $ = require('jquery');
    var Class = require('mod/class');
    var EventBase = require('mod/eventBase');

    var Demo = window.demo = Class({
        instanceMembers: {
            init: function (element,options) {
                this.$element = $(element);
                this.base(this.$element);

                //添加監聽
                this.on('beforeRender', $.proxy(options.onBeforeRender, this));
                this.on('render', $.proxy(options.onRender, this));
            },
            render: function () {
                //觸發beforeRender事件
                var e = $.Event('beforeRender');
                this.trigger(e);
                if(e.isDefaultPrevented())return;
                //主要邏輯代碼
                console.log('render complete!');
                //觸發render事件
                this.trigger('render');
            }
        },
        extend: EventBase
    });

    var demo = new Demo('#demo', {
        onBeforeRender: function(e) {
            console.log('beforeRender event triggered!');
        },
        onRender: function(e) {
            console.log('render event triggered!');
        }
    });

    demo.on('beforeRender', function(e) {
        e.preventDefault();
        console.log('beforeRender event triggered 2!');
    });

    demo.on('beforeRender', function(e) {
        console.log('beforeRender event triggered 3!');
    });

    demo.render();
});

在這個測試了, 我定義了一個跟DOM相關的Demo組件並繼承了EventBase這個事件管理的類,給beforeRender事件添加了3個監聽,其中一個有加prevetDefault()的調用,而且該回調還不是最後一個,最後的測試結果是:
image
從結果可以看到,render方法的主要邏輯代碼跟後面的render事件都沒有執行,所有beforeRender的監聽器都執行了,說明e.preventDefault()生效了,而且它沒有對beforeRender的事件隊列產生影響。

2)模擬跟DOM無關聯的普通對象

測試代碼一:

define(function(require, exports, module) {
    var $ = require('jquery');
    var Class = require('mod/class');
    var EventBase = require('mod/eventBase');

    var Demo = window.demo = Class({
        instanceMembers: {
            init: function (options) {
                this.base();

                //添加監聽
                this.on('beforeRender', $.proxy(options.onBeforeRender, this));
                this.on('render', $.proxy(options.onRender, this));
            },
            render: function () {
                //觸發beforeRender事件
                var e = $.Event('beforeRender');
                this.trigger(e);
                if(e.isDefaultPrevented())return;
                //主要邏輯代碼
                console.log('render complete!');
                //觸發render事件
                this.trigger('render');
            }
        },
        extend: EventBase
    });

    var demo = new Demo({
        onBeforeRender: function(e) {
            console.log('beforeRender event triggered!');
        },
        onRender: function(e) {
            console.log('render event triggered!');
        }
    });

    demo.render();
});

在這個測試里, 我定義了一個跟DOM無關的Demo組件並繼承了EventBase這個事件管理的類,給beforeRender事件和render事件都添加了一個監聽,render方法中也有列印信息來模擬真實的邏輯,最後的測試結果是:image

完全與預期的一致。

測試代碼二:

define(function(require, exports, module) {
    var $ = require('jquery');
    var Class = require('mod/class');
    var EventBase = require('mod/eventBase');

    var Demo = window.demo = Class({
        instanceMembers: {
            init: function (options) {
                this.base();

                //添加監聽
                this.on('beforeRender', $.proxy(options.onBeforeRender, this));
                this.on('render', $.proxy(options.onRender, this));
            },
            render: function () {
                //觸發beforeRender事件
                var e = $.Event('beforeRender');
                this.trigger(e);
                if(e.isDefaultPrevented())return;
                //主要邏輯代碼
                console.log('render complete!');
                //觸發render事件
                this.trigger('render');
            }
        },
        extend: EventBase
    });

    var demo = new Demo({
        onBeforeRender: function(e) {
            console.log('beforeRender event triggered!');
        },
        onRender: function(e) {
            console.log('render event triggered!');
        }
    });

    demo.on('beforeRender', function(e) {
        e.preventDefault();
        console.log('beforeRender event triggered 2!');
    });

    demo.on('beforeRender', function(e) {
        console.log('beforeRender event triggered 3!');
    });

    demo.render();
});

在這個測試了, 我定義了一個跟DOM無關的Demo組件並繼承了EventBase這個事件管理的類,給beforeRender事件添加了3個監聽,其中一個有加prevetDefault()的調用,而且該回調還不是最後一個,最後的測試結果是:
image
從結果可以看到,render方法的主要邏輯代碼跟後面的render事件都沒有執行,所有beforeRender的監聽器都執行了,說明e.preventDefault()生效了,而且它沒有對beforeRender的事件隊列產生影響。

所以從2個測試來看,通過改造後的EventBase,我們得到了一個可以讓任意對象支持jquery事件管理機制的方法,將來在考慮用事件機制來解耦的時候,就不用再去考慮前面第一個介紹的發佈-訂閱模式了,而且相對而言這個方法功能更強更穩定,也更符合你平常使用jquery操作DOM的習慣。

4. 本文小結

有2點需要再說明一下的是:

1)即使不用jquery按照第1部分最後提出的思路,把第一部分常規的發佈-訂閱模式改造一下也可以的,只不過用jquery更加簡潔些;
2)最終用jquery 的事件機制來實現任意對象的事件管理,一方面是用到了代理模式,更重要的還是要用發佈-訂閱模式,只不過最後的這個實現是由jquery幫我們把第一部分的發佈-訂閱實現改造好了而已。

最後真切地希望這篇分享能夠給你的工作帶來一些幫助,謝謝閱讀:)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • oauth應該屬於security的一部分。關於oauth的的相關知識可以查看阮一峰的文章:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 一、目標 現在很多系統都支持第三方賬號密碼等登陸我們自己的系統,例如:我們經常會看到,一些系統使用微 ...
  • 最近遇到的問題小結: 1.django 工程內不要有與項目名稱相同的文件。會導致無法import settings.py文件。 2.django 的 csrf 問題,當發送post請求時,會要求同時發送csrf token,是為了防止跨站請求偽造。 具體使用方法見官方文檔。 http://docs. ...
  • 介紹 我發現了一個問題,今天與大家分享。我把整個過程描述一下。 問題 問題 公司有個框架是基於smarty寫的,我負責php的升級,維護人員把新環境布上來之後,測試人員找我提出經常報錯(錯誤:提示找不到文件的)。 我追蹤了一下代碼,原來是smarty的這個地方報的錯誤。 錯誤:這裡報出文件不存在。 ...
  • 開篇導讀 “養成良好的編程習慣”其實是相當綜合的一個命題,可以從多個角度、維度和層次進行論述和評判。如代碼的風格、效率和可讀性;模塊設計的靈活性、可擴展性和耦合度等等。要試圖把所有方面都闡述清楚必須花很多的精力,而且也不一定能闡述得全面。因此,本系列文章以軟體開發的基礎問題為切入點,闡述程式設計和代 ...
  • 初始化一個map 1 2 3 4 5 Map<String, String> map = new HashMap<String, String>(); map.put("1", "hell"); map.put("2", "hello"); map.put("3", "hel"); map.put( ...
  • 幹掉這道題的那一刻,我只想說:我終於**的AC了!!! 最終記憶體1344K,耗時10282ms,比起歸併樹、劃分樹以及其他各種黑科技,這個成績並不算光彩⊙﹏⊙ 但至少,從最初的無數次TLE到最終的AC,這過程見證了一個二分演算法的艱辛優化 先貼代碼: 1 const int bktSize=1024; ...
  • 現在編程的主流語言基本上都是面向對象的。如C#,C++,JAVA。我們在使用時,已經構造了一個個的類。但是往往由於我們在類內部或外部的設計上存在種種問題,導致儘管是面向對象的語言,卻是面向過程的邏輯,甚至維護起來異常困難。每次增加或修改功能都要改動很多的代碼,如履薄冰。而面向對象的六大原則主要的目的 ...
  • 一、設計目的 從事.Net平臺開發系統已有8年多了,一直思考搭建.Net分散式系統架構。基於window平臺搭建的大型分散式系統不多,之前瞭解過myspace、stackoverflow等大型網站。搭建一個大型平臺需要綜合考慮很多方面,不單純是軟體架構,還包括網路和硬體設備等。由於現代大部分應用建設 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...