网络编程
位置:首页>> 网络编程>> JavaScript>> 利用Javascript实现一套自定义事件机制

利用Javascript实现一套自定义事件机制

作者:依韵  发布时间:2024-05-03 15:59:07 

标签: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';
}
});

在控件中使用事件机制,即可简化开发,使得流程易于控制,还可为实际使用时提供非常丰富的个性化操作,快快用起来吧。

来源:https://blog.cdswyda.com/post/20171027

0
投稿

猜你喜欢

  • 触发器:触发器的使用场景以及相应版本:触发器可以使用的MySQL版本:版本:MySQL5以上使用场景例子:每当增加一个顾客到某个数据库表时,
  • python中查找指定的字符串的方法如下:code#查询def selStr():  sStr1 = 'jsjtt.com
  • 现有表格中的一行的代码如下所示: 效果可以看下具体51搜索展示http://www.51bt.cc,结合Xunsearch全文检索技术,可以
  • 一个 MySQL 表可以看作是一个队列,每一行为一个元素。每次查询得到满足某个条件的最前面的一行,并将它从表中删除或者改变它的状态,使得下次
  • 1.列表推导式书写形式:[表达式 for 变量 in 列表]    或者  [表达式 for 变量
  • 这个功能需要写一点代码来实现。下面的函数可以得到一个变量的类型,调用时传递一个变量进去,会返回用字符串形式描述的变量类型。//得到x的类型,
  • 第一种:拼接字符串,可以解决问题,但是为了避免sql注入,不建议这样写还是看看第二种:使用.format()函数,很多时候我都是使用这个函数
  • f-string,亦称为格式化字符串常量(formatted string literals),是Python3.6新引入的一种字符串格式化
  •  如果你想用Python开发Windows程序,并让其开机启动等,就必须写成windows的服务程序Windows Service
  • 用python实现文件夹下的成批文件格式转换我们对于文件转换的需求很大,甚至于对于图片的格式,JPG和PNG格式在肉眼看来都没什么差别,但是
  • 可以自动轮换的页签 tabs with auto play fucntion<html><head><meta
  • 1.前言面向对象编程的三大特性:封装、继承、多态。可见继承是面向对象程序设计中一个重要的概念。Go 作为面向对象的编程语言,自然也支持继承。
  • 实训课期间忙里偷闲的学习了python的selenium包,唯一一点不好是要自己去查英文文档,明摆着欺负我这种英语不好的,想着用谷歌翻译一下
  • oracle如果存储过程比较复杂,我们要定位到错误就比较困难,那么可以存储过程的调试功能先按简单的存储过程做个例子,就是上次做的存储过程(p
  • 有时需要获取远程网站的某些信息,而服务器又限制了GET方式,只能通过POST数据提交,这个时候我们可以通过asp来实现模拟提交post数据,
  • 问题现在有多个字典或者映射,你想将它们从逻辑上合并为一个单一的映射后执行某些操作,比如查找值或者检查某些键是否存在。解决方案加入你有如下两个
  • 本文实例为大家分享了python实现名片管理系统的具体代码,供大家参考,具体内容如下名片管理系统前提:实现名片管理系统,首先要创建两个pyt
  • 如何为XHTML做好准备,XHTML与HTML 4.01标准没有太多的不同。所以将你的代码升级至4.01是个不错的开始。HTML 4.01参
  • using System;using System.Data.SqlClient;namespace ExecuteSqlTran{&nbs
  • 提到SQL Server 2005证书,很多人可能以为它只是用来在传输数据的时候起到加密作用的,但在深入了解后,你会发现它的用处还有很多。
手机版 网络编程 asp之家 www.aspxhome.com