隨著web技術發展,使用JavaScript自訂物件愈發頻繁,讓自己創建的物件也有事件機制,透過事件對外通信,能夠大幅提高開發效率。以下這篇文章主要為大家介紹了利用Javascript實作一套自訂事件機制的相關資料,需要的朋友可以參考下。
前言
事件機制為我們的web開發提供了極大的方便,使得我們能在任何時候指定在什麼操作時做什麼操作、執行什麼樣的程式碼。
如點擊事件,使用者點擊時觸發;keydown、keyup事件,鍵盤按下、鍵盤彈起時觸發;還有上傳控制項中,檔案加入前事件,上傳完成後事件。
由於在適當的時機會有對應的事件觸發,我們能為這些事件指定對應的處理函數,就能在原本的流程中插入各種各樣的個人化操作和處理,使得整個流程變得更加豐富。
諸如click、blur、focus等事件是原本的dom就直接提供的原生事件,而我們使用的一些其他控制項所使用的各種事件則不是原生dom就有的,如上傳控制項中通常都會有上傳開始和完成事件,那麼這些事件是如何實現的呢?
也想在自己的開發的控制項中加入類似的事件機制該如何實現呢?就讓我們來一探究竟。
事件應有的功能
在實作之前,我們先來分析事件機制應該有的基本功能。
簡單來說,事件必須要提供以下幾種功能:
綁定事件
觸發事件
取消綁定事件
#前期準備
我們來觀察一下事件的一個特徵,事件必定是屬於某個對象的。如:focus和blur事件是可取得焦點的dom元素的,input事件是輸入框的,上傳開始和上傳成功則是上傳成功的。
也就是說,事件不是獨立存在的,它需要一個載體。那我們要怎麼讓事件有一個載體呢?一個簡單的實作方案則是,將事件當作一個基類,在需要事件的地方繼承這個事件類別即可。
我們將綁定事件、觸發事件、取消綁定事件分別命名為:on、fire、off,那麼我們可以簡單寫出這個事件類別:
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, // event = { // // 事件类型 // type: type, // // 绑定的源 // origin: this, // // scope 为 this 或用户指定的上下文, // // 其他数据 // data: data, // // 是否取消 // cancel: false // }; // 上面对自定义参数的处理不便于使用 将相关属性直接合并到事件参数中 event = $.extend({ // 事件类型 type: type, // 绑定的源 origin: this, // scope 为 this 或用户指定的上下文, // 其他数据 // data: data, // 是否取消 cancel: false }, data); if (!eventArr) { return event; } for (var i = 0, l = eventArr.length; i < l; ++i) { fn = eventArr[i][0]; event.scope = eventArr[i][1] || this; fn.call(event.scope, 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); // 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); // 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 () {}, // 日历的其他实现 。。。 }
以上伪代码作为示意,仅需在让控件继承到on、off 、fire 、one等方法即可。但是必须保证事件的存储对象_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'; } });
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是使用Javascript如何實作自訂事件機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!