摘要:因此事件觸發(fā)時,事件處理函數(shù)的實參中必須包含當(dāng)前事件的基本信息。事件取消事件取消中需要做的就是已經(jīng)綁定的事件處理函數(shù)移除掉即可。
事件機(jī)制為我們的web開發(fā)提供了極大的方便,使得我們能在任意時候指定在什么操作時做什么操作、執(zhí)行什么樣的代碼。
如點擊事件,用戶點擊時觸發(fā);keydown、keyup事件,鍵盤按下、鍵盤彈起時觸發(fā);還有上傳控件中,文件加入前事件,上傳完成后事件。
由于在恰當(dāng)?shù)臅r機(jī)會有相應(yīng)的事件觸發(fā),我們能為這些事件指定相應(yīng)的處理函數(shù),就能在原本的流程中插入各種各樣的個性化操作和處理,使得整個流程變得更加豐富。
諸如click、blur、focus等事件是原本的dom就直接提供的原生事件,而我們使用的一些其他控件所使用的各種事件則不是原生dom就有的,如上傳控件中通常都會有上傳開始和完成事件,那么這些事件都是如何實現(xiàn)的呢?
也想在自己的開發(fā)的控件中加入類似的事件機(jī)制該如何實現(xiàn)呢? 就讓我們來一探究竟。
事件應(yīng)有的功能在實現(xiàn)之前,我們首先來分析事件機(jī)制應(yīng)該有的基本功能。
簡單來說,事件必須要提供以下幾種功能:
綁定事件
觸發(fā)事件
取消綁定事件
前期準(zhǔn)備我們來觀察一下事件的一個特征,事件必定是屬于某個對象的。如:focus和blur事件是可獲取焦點的dom元素的,input事件是輸入框的,上傳開始和上傳成功則是上傳成功的。
也就是說,事件不是獨立存在的,它需要一個載體。那么我們怎么讓事件有一個載體呢?一種簡單的實現(xiàn)方案則是,將事件作為一個基類,在需要事件的地方繼承這個事件類即可。
我們將綁定事件、觸發(fā)事件、取消綁定事件分別命名為:on、fire、off,那么我們可以簡單寫出這個事件類:
function CustomEvent() { this._events = {}; } CustomEvent.prototype = { constructor: CustomEvent, // 綁定事件 on: function () { }, // 觸發(fā)事件 fire: function () { }, // 取消綁定事件 off: function () { } };事件綁定
首先來實現(xiàn)事件的綁定,事件綁定必須要指定事件的類型和事件的處理函數(shù)。
那么除此之外還需要什么呢?我們是自定義事件,不需要像原生事件一樣指定是冒泡階段觸發(fā)還是捕獲階段觸發(fā),也不需要像jQuery里一樣可以額外指定那些元素觸發(fā)。
而事件函數(shù)里面this一般都是當(dāng)前實例,這個在某些情況下可能不適用,我們需要重新指定事件處理函數(shù)運行時的上下文環(huán)境。
因此確定事件綁定時三個參數(shù)分別為:事件類型、事件處理函數(shù)、事件處理函數(shù)執(zhí)行上下文。
那么事件綁定要干什么呢,其實很簡單,事件綁定只用將相應(yīng)的事件名稱和事件處理函數(shù)記錄下來即可。
我的實現(xiàn)如下:
{ /** * 綁定事件 * * @param {String} type 事件類型 * @param {Function} fn 事件處理函數(shù) * @param {Object} scope 要為事件處理函數(shù)綁定的執(zhí)行上下文 * @returns 當(dāng)前實例對象 */ 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; } }
由于一種事件可以綁定多次,執(zhí)行時依次執(zhí)行,所有事件類型下的處理函數(shù)存儲使用的是數(shù)組。
事件觸發(fā)事件觸發(fā)的基本功能就是去執(zhí)行用戶所綁定的事件,所以只用在事件觸發(fā)時去檢查有沒有指定的執(zhí)行函數(shù),如果有則調(diào)用即可。
另外事件觸發(fā)實際就是用戶指定的處理函數(shù)執(zhí)行的過程,而能進(jìn)行很多個性化操作也都是在用戶指定的事件處理函數(shù)中進(jìn)行的,因此僅僅是執(zhí)行這個函數(shù)還不夠。還必須為當(dāng)前函數(shù)提供必要的信息,如點擊事件中有當(dāng)前被點擊的元素,鍵盤事件中有當(dāng)前鍵的鍵碼,上傳開始和上傳完成中有當(dāng)前文件的信息。
因此事件觸發(fā)時,事件處理函數(shù)的實參中必須包含當(dāng)前事件的基本信息。
除此之外通過用戶在事件處理函數(shù)中的操作,可能需要調(diào)整之后的信息,如keydwon事件中用戶可以禁止此鍵的錄入,文件上傳前,用戶在事件中取消此文件的上傳或是修改一些文件信息。因此事件觸發(fā)函數(shù)應(yīng)返回用戶修改后的事件對象。
我的實現(xiàn)如下:
{ /** * 觸發(fā)事件 * * @param {String} type 觸發(fā)事件的名稱 * @param {Object} data 要額外傳遞的數(shù)據(jù),事件處理函數(shù)參數(shù)如下 * event = { // 事件類型 type: type, // 綁定的源,始終為當(dāng)前實例對象 origin: this, // 事件處理函數(shù)中的執(zhí)行上下文 為 this 或用戶指定的上下文對象 scope :this/scope // 其他數(shù)據(jù) 為fire時傳遞的數(shù)據(jù) } * @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; } }
上面實現(xiàn)中給事件處理函數(shù)的實參中必定包含以下信息:
type : 當(dāng)前觸發(fā)的事件類型
origin : 當(dāng)前事件綁定到的對象
scope : 事件處理函數(shù)的執(zhí)行上下文
此外不同事件在各種的觸發(fā)時可為此事件對象中加入各自不同的信息。
關(guān)于 Object.assign(target, ...sources) 是ES6中的一個方法,作用是將所有可枚舉屬性的值從一個或多個源對象復(fù)制到目標(biāo)對象,并返回目標(biāo)對象,類似于大家熟知的$.extend(target,..sources) 方法。
事件取消事件取消中需要做的就是已經(jīng)綁定的事件處理函數(shù)移除掉即可。
實現(xiàn)如下:
{ /** * 取消綁定一個事件 * * @param {String} type 取消綁定的事件名稱 * @param {Function} fn 要取消綁定的事件處理函數(shù),不指定則移除當(dāng)前事件類型下的全部處理函數(shù) * @returns 當(dāng)前實例對象 */ 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 可能存在一個事件一個函數(shù)綁定多次的情況 // 刪除后數(shù)組改變,下一個仍然需要遍歷處理! --i; } } } return this; } }
此處實現(xiàn)類似原生的事件取消綁定,如果指定了事件處理函數(shù)則移除指定事件的指定處理函數(shù),如果省略事件處理函數(shù)則移除當(dāng)前事件類型下的所有事件處理函數(shù)。
僅觸發(fā)一次的事件jQuery中有一個 one 方法,它所綁定的事件僅會執(zhí)行一次,此方法在一些特定情況下非常有用,不需要用戶手動取消綁定這個事件。
這里的實現(xiàn)也非常簡單,只用在觸發(fā)這個事件時取消綁定即可。
實現(xiàn)如下:
{ /** * 綁定一個只執(zhí)行一次的事件 * * @param {String} type 事件類型 * @param {Function} fn 事件處理函數(shù) * @param {Object} scope 要為事件處理函數(shù)綁定的執(zhí)行上下文 * @returns 當(dāng)前實例對象 */ one: function (type, fn, scope) { var that = this; function nfn() { // 執(zhí)行時 先取消綁定 that.off(type, nfn); // 再執(zhí)行函數(shù) fn.apply(scope || that, arguments); } this.on(type, nfn, scope); return this; } }
原理則是不把用戶指定的函數(shù)直接綁定上去,而是生成一個新的函數(shù),并綁定,此函數(shù)執(zhí)行時會先取消綁定,再執(zhí)行用戶指定的處理函數(shù)。
基本雛形到此,一套完整的事件機(jī)制就已經(jīng)完成了,完整代碼如下:
function CustomEvent() { this._events = {}; } CustomEvent.prototype = { constructor: CustomEvent, /** * 綁定事件 * * @param {String} type 事件類型 * @param {Function} fn 事件處理函數(shù) * @param {Object} scope 要為事件處理函數(shù)綁定的執(zhí)行上下文 * @returns 當(dāng)前實例對象 */ 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; }, /** * 觸發(fā)事件 * * @param {String} type 觸發(fā)事件的名稱 * @param {Anything} data 要額外傳遞的數(shù)據(jù),事件處理函數(shù)參數(shù)如下 * event = { // 事件類型 type: type, // 綁定的源,始終為當(dāng)前實例對象 origin: this, // 事件處理函數(shù)中的執(zhí)行上下文 為 this 或用戶指定的上下文對象 scope :this/scope // 其他數(shù)據(jù) 為fire時傳遞的數(shù)據(jù) } * @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 要取消綁定的事件處理函數(shù),不指定則移除當(dāng)前事件類型下的全部處理函數(shù) * @returns 當(dāng)前實例對象 */ 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 可能存在一個事件一個函數(shù)綁定多次的情況 // 刪除后數(shù)組改變,下一個仍然需要遍歷處理! --i; } } } return this; }, /** * 綁定一個只執(zhí)行一次的事件 * * @param {String} type 事件類型 * @param {Function} fn 事件處理函數(shù) * @param {Object} scope 要為事件處理函數(shù)綁定的執(zhí)行上下文 * @returns 當(dāng)前實例對象 */ one: function (type, fn, scope) { var that = this; function nfn() { // 執(zhí)行時 先取消綁定 that.off(type, nfn); // 再執(zhí)行函數(shù) fn.apply(scope || that, arguments); } this.on(type, nfn, scope); return this; } };在自己的控件中使用
上面已經(jīng)實現(xiàn)了一套事件機(jī)制,我們?nèi)绾卧谧约旱氖录惺褂媚亍?/p>
比如我寫了一個日歷控件,需要使用事件機(jī)制。
function Calendar() { // 加入事件機(jī)制的存儲的對象 this._event = {}; // 日歷的其他實現(xiàn) } Calendar.prototype = { constructor:Calendar, on:function () {}, off:function () {}, fire:function () {}, one:function () {}, // 日歷的其他實現(xiàn) 。。。 }
以上偽代碼作為示意,僅需在讓控件繼承到on、off 、fire 、one等方法即可。但是必須保證事件的存儲對象_events 必須是直接加載實例上的,這點需要在繼承時注意,JavaScript中實現(xiàn)繼承的方案太多了。
上面為日歷控件Calendar中加入了事件機(jī)制,之后就可以在Calendar中使用了。
如在日歷開發(fā)時,我們在日歷的單元格渲染時觸發(fā)cellRender事件。
// 每天渲染時發(fā)生 還未插入頁面 var renderEvent = this.fire("cellRender", { // 當(dāng)天的完整日期 date: date.format("YYYY-MM-DD"), // 當(dāng)天的iso星期 isoWeekday: day, // 日歷dom el: this.el, // 當(dāng)前單元格 tdEl: td, // 日期文本 dateText: text.innerText, // 日期class dateCls: text.className, // 需要注入的額外的html extraHtml: "", isHeader: false });
在事件中,我們將當(dāng)前渲染的日期、文本class等信息都提供給用戶,這樣用戶就可以綁定這個事件,在這個事件中進(jìn)行自己的個性阿化處理了。
如在渲染時,如果是周末則插入一個"假"的標(biāo)識,并讓日期顯示為紅色。
var calendar = new Calendar();
calendar.on("cellRender", function (e) {
if(e.isoWeekday > 5 ) {
e.extraHtml = "假";
e.dateCls += " red";
}
});
在控件中使用事件機(jī)制,即可簡化開發(fā),使得流程易于控制,還可為實際使用時提供非常豐富的個性化操作,快快用起來吧。
原文首發(fā)我的博客 https://blog.cdswyda.com/post/20171027
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/89368.html
摘要:直接使用事件代理機(jī)制,將事件綁定在整個日歷的上即可,這樣事件只用在創(chuàng)建時初始化一次即可,簡單高效省內(nèi)存。 首發(fā)我的博客 - https://blog.cdswyda.com/post/2017121010 日歷控件多的不勝枚舉,為什么我們還要再造一個輪子呢? 因為大多數(shù)日歷控件都是用于選擇日期的,有種需求是要在日歷上展示各種各樣的內(nèi)容,這樣的日歷控件較少,而且試用下來并不滿意。 因此就...
摘要:不過也有自己的一套自定義事件方案??梢院褪录脕韺Ρ?,他們都是用來模擬和執(zhí)行監(jiān)聽的事件。冒泡事件就是就是由內(nèi)向外冒泡的過程,這個過程不是很復(fù)雜。參考解密事件核心自定義設(shè)計三解密事件核心模擬事件四本文在上的源碼地址,歡迎來。 歡迎來我的專欄查看系列文章。 以前,我只知道,只有當(dāng)對瀏覽器中的元素進(jìn)行點擊的時候,才會出發(fā) click 事件,其它的事件也一樣,需要人為的鼠標(biāo)操作。 showIm...
摘要:不過按照經(jīng)驗來說,這類異常要么盡量避免,要么出現(xiàn)了就要做異常處理,從而保證程序的健壯性。業(yè)務(wù)是千變?nèi)f化,但是它們可能產(chǎn)生的異常處理方式是不會變化的,按照這個思路去做異常處理即可。 前言:說到異常體系,可能對于一些初入職場的老鐵會很頭痛,不能夠很清晰的描述異常是個什么情況。那么本文將通過打流水仗的方式給大家介紹一下工作中涉及的異常知識。首先能看到本文,說明也對異常是有了解的,所以文章開頭...
閱讀 1641·2021-09-02 09:55
閱讀 1115·2019-08-30 13:19
閱讀 1403·2019-08-26 13:51
閱讀 1453·2019-08-26 13:49
閱讀 2383·2019-08-26 12:13
閱讀 462·2019-08-26 11:52
閱讀 1910·2019-08-26 10:58
閱讀 3090·2019-08-26 10:19