# 實現一套自定義事件機制

事件機制爲咱們的web開發提供了極大的方便,使得咱們能在任意時候指定在什麼操做時作什麼操做、執行什麼樣的代碼。html

如點擊事件,用戶點擊時觸發;keydown、keyup事件,鍵盤按下、鍵盤彈起時觸發;還有上傳控件中,文件加入前事件,上傳完成後事件。web

因爲在恰當的時機會有相應的事件觸發,咱們能爲這些事件指定相應的處理函數,就能在本來的流程中插入各類各樣的個性化操做和處理,使得整個流程變得更加豐富。數組

諸如click、blur、focus等事件是本來的dom就直接提供的原生事件,而咱們使用的一些其餘控件所使用的各類事件則不是原生dom就有的,如上傳控件中一般都會有上傳開始和完成事件,那麼這些事件都是如何實現的呢?app

也想在本身的開發的控件中加入相似的事件機制該如何實現呢? 就讓咱們來一探究竟。dom

事件應有的功能

在實現以前,咱們首先來分析事件機制應該有的基本功能。函數

簡單來講,事件必需要提供如下幾種功能:post

  1. 綁定事件
  2. 觸發事件
  3. 取消綁定事件

前期準備

咱們來觀察一下事件的一個特徵,事件一定是屬於某個對象的。如:focus和blur事件是可獲取焦點的dom元素的,input事件是輸入框的,上傳開始和上傳成功則是上傳成功的。ui

也就是說,事件不是獨立存在的,它須要一個載體。那麼咱們怎麼讓事件有一個載體呢?一種簡單的實現方案則是,將事件做爲一個基類,在須要事件的地方繼承這個事件類便可。this

咱們將綁定事件、觸發事件、取消綁定事件分別命名爲:onfireoff,那麼咱們能夠簡單寫出這個事件類:spa

function CustomEvent() {
    this._events = {};
}

CustomEvent.prototype = {
    constructor: CustomEvent,
    // 綁定事件
    on: function () {

    },
    // 觸發事件
    fire: function () {

    },
    // 取消綁定事件
    off: function () {

    }
};

事件綁定

首先來實現事件的綁定,事件綁定必需要指定事件的類型和事件的處理函數。

那麼除此以外還須要什麼呢?咱們是自定義事件,不須要像原生事件同樣指定是冒泡階段觸發仍是捕獲階段觸發,也不須要像jQuery裏同樣能夠額外指定那些元素觸發。

而事件函數裏面this通常都是當前實例,這個在某些狀況下可能不適用,咱們須要從新指定事件處理函數運行時的上下文環境。

所以肯定事件綁定時三個參數分別爲:事件類型、事件處理函數、事件處理函數執行上下文。

那麼事件綁定要幹什麼呢,其實很簡單,事件綁定只用將相應的事件名稱和事件處理函數記錄下來便可。

個人實現以下:

{
    /**
     * 綁定事件
     * 
     * @param {String} type 事件類型
     * @param {Function} fn 事件處理函數
     * @param {Object} scope 要爲事件處理函數綁定的執行上下文
     * @returns 當前實例對象
     */
    on: function (type, fn, scope) {
        if (type + '' !== type) {
            console && console.error && console.error('the first argument type is requird as string');
            return this;
        }

        if (typeof fn != 'function') {
            console && console.error && console.error('the second argument fn is requird as function');
            return this;
        }

        type = type.toLowerCase();

        if (!this._events[type]) {
            this._events[type] = [];
        }

        this._events[type].push(scope ? [fn, scope] : [fn]);

        return this;
    }
}

因爲一種事件能夠綁定屢次,執行時依次執行,全部事件類型下的處理函數存儲使用的是數組。

事件觸發

事件觸發的基本功能就是去執行用戶所綁定的事件,因此只用在事件觸發時去檢查有沒有指定的執行函數,若是有則調用便可。

另外事件觸發實際就是用戶指定的處理函數執行的過程,而能進行不少個性化操做也都是在用戶指定的事件處理函數中進行的,所以僅僅是執行這個函數還不夠。還必須爲當前函數提供必要的信息,如點擊事件中有當前被點擊的元素,鍵盤事件中有當前鍵的鍵碼,上傳開始和上傳完成中有當前文件的信息。

所以事件觸發時,事件處理函數的實參中必須包含當前事件的基本信息。

除此以外經過用戶在事件處理函數中的操做,可能須要調整以後的信息,如keydwon事件中用戶能夠禁止此鍵的錄入,文件上傳前,用戶在事件中取消此文件的上傳或是修改一些文件信息。所以事件觸發函數應返回用戶修改後的事件對象。

個人實現以下:

{
    /**
     * 觸發事件
     * 
     * @param {String} type 觸發事件的名稱
     * @param {Object} data 要額外傳遞的數據,事件處理函數參數以下
     * event = {
            // 事件類型
            type: type,
            // 綁定的源,始終爲當前實例對象
            origin: this,
            // 事件處理函數中的執行上下文 爲 this 或用戶指定的上下文對象
            scope :this/scope
            // 其餘數據 爲fire時傳遞的數據
        }
     * @returns 事件對象
     */
    fire: function (type, data) {
        type = type.toLowerCase();

        var eventArr = this._events[type];

        var fn, scope,

            event = Object.assign({
                // 事件類型
                type: type,
                // 綁定的源
                origin: this,
                // scope 爲 this 或用戶指定的上下文,
                // 是否取消
                cancel: false
            }, data);

        if (!eventArr) return event;

        for (var i = 0, l = eventArr.length; i < l; ++i) {
            fn = eventArr[i][0];
            scope = eventArr[i][1];
            if (scope) {
                event.scope = scope;
                fn.call(scope, event);
            } else {
                event.scope = this;
                fn(event);
            }
        }
        return event;
    }
}

上面實現中給事件處理函數的實參中一定包含如下信息:

  • type : 當前觸發的事件類型
  • origin : 當前事件綁定到的對象
  • scope : 事件處理函數的執行上下文

此外不一樣事件在各類的觸發時可爲此事件對象中加入各自不一樣的信息。

關於 Object.assign(target, ...sources) 是ES6中的一個方法,做用是將全部可枚舉屬性的值從一個或多個源對象複製到目標對象,並返回目標對象,相似於你們熟知的$.extend(target,..sources) 方法。

事件取消

事件取消中須要作的就是已經綁定的事件處理函數移除掉便可。

實現以下:

{
    /**
     * 取消綁定一個事件
     * 
     * @param {String} type 取消綁定的事件名稱
     * @param {Function} fn 要取消綁定的事件處理函數,不指定則移除當前事件類型下的所有處理函數
     * @returns 當前實例對象
     */
    off: function (type, fn) {
        type = type.toLowerCase();

        var eventArr = this._events[type];

        if (!eventArr || !eventArr.length) return this;

        if (!fn) {
            this._events[type] = eventArr = [];
        } else {
            for (var i = 0; i < eventArr.length; ++i) {
                if (fn === eventArr[i][0]) {
                    eventArr.splice(i, 1);
                    // 一、找到後不能當即 break 可能存在一個事件一個函數綁定屢次的狀況
                    // 刪除後數組改變,下一個仍然須要遍歷處理!
                    --i;
                }
            }
        }
        return this;
    }
}

此處實現相似原生的事件取消綁定,若是指定了事件處理函數則移除指定事件的指定處理函數,若是省略事件處理函數則移除當前事件類型下的全部事件處理函數。

僅觸發一次的事件

jQuery中有一個 one 方法,它所綁定的事件僅會執行一次,此方法在一些特定狀況下很是有用,不須要用戶手動取消綁定這個事件。

這裏的實現也很是簡單,只用在觸發這個事件時取消綁定便可。

實現以下:

{
    /**
     * 綁定一個只執行一次的事件
     * 
     * @param {String} type 事件類型
     * @param {Function} fn 事件處理函數
     * @param {Object} scope 要爲事件處理函數綁定的執行上下文
     * @returns 當前實例對象
     */
    one: function (type, fn, scope) {
        var that = this;

        function nfn() {
            // 執行時 先取消綁定
            that.off(type, nfn);
            // 再執行函數
            fn.apply(scope || that, arguments);
        }

        this.on(type, nfn, scope);

        return this;
    }
}

原理則是不把用戶指定的函數直接綁定上去,而是生成一個新的函數,並綁定,此函數執行時會先取消綁定,再執行用戶指定的處理函數。

基本雛形

到此,一套完整的事件機制就已經完成了,完整代碼以下:

function CustomEvent() {
    this._events = {};
}

CustomEvent.prototype = {
    constructor: CustomEvent,
    /**
     * 綁定事件
     * 
     * @param {String} type 事件類型
     * @param {Function} fn 事件處理函數
     * @param {Object} scope 要爲事件處理函數綁定的執行上下文
     * @returns 當前實例對象
     */
    on: function (type, fn, scope) {
        if (type + '' !== type) {
            console && console.error && console.error('the first argument type is requird as string');
            return this;
        }

        if (typeof fn != 'function') {
            console && console.error && console.error('the second argument fn is requird as function');
            return this;
        }

        type = type.toLowerCase();

        if (!this._events[type]) {
            this._events[type] = [];
        }

        this._events[type].push(scope ? [fn, scope] : [fn]);

        return this;
    },
    /**
     * 觸發事件
     * 
     * @param {String} type 觸發事件的名稱
     * @param {Anything} data 要額外傳遞的數據,事件處理函數參數以下
     * event = {
            // 事件類型
            type: type,
            // 綁定的源,始終爲當前實例對象
            origin: this,
            // 事件處理函數中的執行上下文 爲 this 或用戶指定的上下文對象
            scope :this/scope
            // 其餘數據 爲fire時傳遞的數據
        }
        * @returns 事件對象
        */
    fire: function (type, data) {
        type = type.toLowerCase();

        var eventArr = this._events[type];

        var fn, scope,

            event = Object.assign({
                // 事件類型
                type: type,
                // 綁定的源
                origin: this,
                // scope 爲 this 或用戶指定的上下文,
                // 是否取消
                cancel: false
            }, data);

        if (!eventArr) return event;

        for (var i = 0, l = eventArr.length; i < l; ++i) {
            fn = eventArr[i][0];
            scope = eventArr[i][1];
            if (scope) {
                event.scope = scope;
                fn.call(scope, event);
            } else {
                event.scope = this;
                fn(event);
            }
        }
        return event;
    },
    /**
     * 取消綁定一個事件
     * 
     * @param {String} type 取消綁定的事件名稱
     * @param {Function} fn 要取消綁定的事件處理函數,不指定則移除當前事件類型下的所有處理函數
     * @returns 當前實例對象
     */
    off: function (type, fn) {
        type = type.toLowerCase();

        var eventArr = this._events[type];

        if (!eventArr || !eventArr.length) return this;

        if (!fn) {
            this._events[type] = eventArr = [];
        } else {
            for (var i = 0; i < eventArr.length; ++i) {
                if (fn === eventArr[i][0]) {
                    eventArr.splice(i, 1);
                    // 一、找到後不能當即 break 可能存在一個事件一個函數綁定屢次的狀況
                    // 刪除後數組改變,下一個仍然須要遍歷處理!
                    --i;
                }
            }
        }
        return this;
    },
    /**
     * 綁定一個只執行一次的事件
     * 
     * @param {String} type 事件類型
     * @param {Function} fn 事件處理函數
     * @param {Object} scope 要爲事件處理函數綁定的執行上下文
     * @returns 當前實例對象
     */
    one: function (type, fn, scope) {
        var that = this;

        function nfn() {
            // 執行時 先取消綁定
            that.off(type, nfn);
            // 再執行函數
            fn.apply(scope || that, arguments);
        }

        this.on(type, nfn, scope);

        return this;
    }
};

在本身的控件中使用

上面已經實現了一套事件機制,咱們如何在本身的事件中使用呢。

好比我寫了一個日曆控件,須要使用事件機制。

function Calendar() {
    // 加入事件機制的存儲的對象
    this._event = {};

    // 日曆的其餘實現
}

Calendar.prototype = {
    constructor:Calendar,
    on:function () {},
    off:function () {},
    fire:function () {},
    one:function () {},
    // 日曆的其餘實現 。。。
}

以上僞代碼做爲示意,僅需在讓控件繼承到onofffireone等方法便可。可是必須保證事件的存儲對象_events 必須是直接加載實例上的,這點須要在繼承時注意,JavaScript中實現繼承的方案太多了。

上面爲日曆控件Calendar中加入了事件機制,以後就能夠在Calendar中使用了。

如在日曆開發時,咱們在日曆的單元格渲染時觸發cellRender事件。

// 天天渲染時發生 還未插入頁面
var renderEvent = this.fire('cellRender', {
    // 當天的完整日期
    date: date.format('YYYY-MM-DD'),
    // 當天的iso星期
    isoWeekday: day,
    // 日曆dom
    el: this.el,
    // 當前單元格
    tdEl: td,
    // 日期文本
    dateText: text.innerText,
    // 日期class
    dateCls: text.className,
    // 須要注入的額外的html
    extraHtml: '',

    isHeader: false
});

在事件中,咱們將當前渲染的日期、文本class等信息都提供給用戶,這樣用戶就能夠綁定這個事件,在這個事件中進行本身的個性阿化處理了。

如在渲染時,若是是週末則插入一個"假"的標識,並讓日期顯示爲紅色。

var calendar = new Calendar();

calendar.on('cellRender', function (e) {
    if(e.isoWeekday > 5 ) {
        e.extraHtml = '<span>假</span>';
        e.dateCls += ' red';
    }  
});

在控件中使用事件機制,便可簡化開發,使得流程易於控制,還可爲實際使用時提供很是豐富的個性化操做,快快用起來吧。

原文首發個人博客 https://blog.cdswyda.com/post/20171027

相關文章
相關標籤/搜索