前言
事件機制為我們的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';
}
});上面是我整理給大家的,希望今後會對大家有幫助。
相關文章:
使用jQuery封裝animate.css(詳細教程)
vue-cli配置文件(詳細教程)
使用Vue2.x如何實現JSON樹
在Angular4中有關CLI的安裝與使用教程
使用JS實現換圖時鐘(詳細教程)