前端事件

前端事件系统(一)

事件是前端之中,非常重要的一个部分。其作用在于对于用户的各种行为进行相应。近日打算对于事件系统进行更为深入的学习,同时,对于这一部分学习的内容进行一个总结。因为浏览器发展至今,事件系统本身已经尤为的复杂了,所以事件这一部分内容可能会将分为很多章来进行总结。本章将对于事件系统,根据个人的经验,以及其他地方学到的东西进行一个归纳,给出一些简单地处理方案,而在后面几章将会引入经典jquery的源码进行阅读。

绑定方式整理

事件系统发展至今,我们常见的对于事件的绑定方式有三种。

  1. 直接将其写在元素标签之中。类似用如下的写法

    <div onClick="function">

    这种方法可以说是非常古老的写法了,不过至今还是会有人在用。对已现在来说其实并不推荐使用这种方法来绑定事件,其不推荐使用的原因将在第二种绑定方法之中说明。

  2. 对于第一种onXXX的方法,也采用如下的方法进行绑定
    el.onclick = function

    这种方法其实和第一种绑定方法本质上是一样的。也就是我们常常所说的dom0事件。之前在一种中也说过,其实现在并不推荐使用这种方法,原因如下

    1. 该方法绑定的事件所执行的回调函数只允许一个,倘若绑定了两个,那么第二个将覆盖掉对于第一个的绑定。
    2. 该方式只支持事件冒泡
    3. 在ie下的该方式的回调函数,并不能像我们往常一样,拥有事件对象参数。
    4. 该方式对于dom3的部分新增事件不支持。同时对于FF的部分私有实现也并不支持。

    其实对于该方式绑定事件来说,前面三点就决定了,我们不能使用该方法进行绑定事件。而对于第四点,dom3的部分新增事件的不支持,其实我们在日常的使用之中,对于不支持的那些事件,我们的使用频率也是很低的。因为许多新事件,可能还未曾进入我们的视野,就已经被废弃了。

  3. 最后就是我们常说的dom2事件系统了

    不过dom2事件系统,现存的拥有两套不同的API,因此,在下面将分为两方。

    ie方面,对于事件的绑定,采用如下的方法

    el.attachEvent("on"+type,callback)

    这是微软对于ie5添加的API(除了事件绑定外,还有相应的解绑,创建,派发等)。他解决了之前采用onXXX方法会导致的只允许一个回调的情况,支持了对于同种事件多个回调的绑定。但是这套方案,其实并没有给前端带来什么好处,当你对一个事件系统进行处理的适合,应该能很深的感觉到这种方式其带来的无数问题,以及对于这些问题的解决,会花费很多很多的心思。大致带来的问题如下

    • 对于dom3事件的不支持
    • this的指向不是被绑定元素,而是(个人感觉这也是this指向极为特殊的一个情况) - 对于多个回调的绑定,其执行顺序却并不是按照理所当然的想的那样按照绑定顺序来执行,而是按照不规律的顺序来执行的。
    • 其event事件对象与w3c的event对象存在极大差异
    • 同样只支持事件冒泡

    w3c方面,对于事件的绑定,采用如下方案

    el.addEventListener(type,callback,[phase])

    这个是我们现代浏览器上使用的方法,ie9开始也对于这套API进行了支持,这应该是我们目前最常为使用的方案,当然这套方法也拥有他的一些问题。

    • 像之前所提到的,新事件本身就是不稳定的。可能还没有进入人们的视野,就已经被废弃掉了
    • 许多浏览器并不遵循w3c的标准,对于一些事件并不予以支持
    • 恶心的前缀标识部分存在于事件名上
    • 因为w3c的标准制定,晚于一些浏览器厂商,因此,对于早期一些版本的浏览器,事件的对象成员同样存在和w3c标准差异的情况

事件系统的处理

事件系统是前端之中,极为核心的一个部分,因此,我们必须对其种种问题进行一个个的处理。先抛开强大的jquery的事件处理,倘若我们要写出一个对于事件系统的处理,并将其投入使用,那么我们至少应当解决如下的几个问题。

  1. 不同浏览器对于事件系统的API支持的问题
  2. IE的this指向问题
  3. 事件对象的差异性问题
  4. IE执行回调的顺序问题

那么,既然整理好了问题,我们现在就可以开始去解决那些问题。

对于不同浏览器API的支持问题:

我们采用条件判断来进行简单地实现就好

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法优先
        target.addEventListener(eventName,callback,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,callback);
    }else{      //最后在考虑使用onXXX形式
        target["on"+eventName] = callback;
    }

    //返回回调函数,方便用于事件解绑
    return callback;

}

function removeEvent(target,eventName,callback,useCapture) {

    if (target.removeEventListener) {
        target.removeEventListener(eventName, callback, useCapture);
    } else if (target.detachEvent) {
        target.detachEvent("on" + eventName, callback);
    } else {      //onXXX的形式直接将其设置为null即可
        target["on" + eventName] = null;
    }

}

这样,对于问题1,可以说算是解决了,而这样的一个事件注册函数,对于对事件系统简易需求的页面,已经很是足够了。不过既然提出了那些问题,那么就一一来进行解决吧。

对于ie下this指向的问题:

说点题外话,关于this的指向,其实很简单的来说,就是是谁调用的,this就指向谁。比较笼统的总结一下,日常this的指向就两种情况,对象中的this,那么就指向其对应的对象,普通函数中的this,就指向window。然而attachEvent的this指向,却是指向window的,因此,我们不得不对其进行修改。实现方式很简单,使用call或者apply,对于this指向进行修改就可以了。因此,上面的代码可以修改如下。

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法优先
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,handler);
    }else{      //最后在考虑使用onXXX形式
        target["on"+eventName] = handler;
    }

    //增加handler函数,在其中对于this指向进行改变,同时,采用handler处理函数来进行事件回调
    function handler(){
        callback.call(target);
    }

    //返回回调函数,方便用于事件解绑
    return handler;

}

function removeEvent(target,eventName,callback,useCapture) {

    if (target.removeEventListener) {
        target.removeEventListener(eventName, callback, useCapture);
    } else if (target.detachEvent) {
        target.detachEvent("on" + eventName, callback);
    } else {      //onXXX的形式直接将其设置为null即可
        target["on" + eventName] = null;
    }

}

事件对象的差异性问题:

event对象以及对其的处理也是事件绑定之中,要进行的一个重点内容。而具体的处理,我们将在之前对于this指向处理之中的handler中来一一进行解决。

大致就是对于target,currentTarget,冒泡,取消默认事件这几部分来进行简单地处理。修改后的代码如下

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法优先
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,handler);
    }else{      //最后在考虑使用onXXX形式
        target["on"+eventName] = handler;
    }

    //处理传入的参数ev
    function handler(event){
        //ie下的事件名需要window.event
        var ev = event || window.event,
            stopPropagation = ev.stopPropagation,
            preventDefault = ev.preventDefault;

        //获取触发事件的对象 ie下的ev.srcElement相当于其他浏览器下ev.target
        ev.target = ev.target || ev.srcElement;
        //获取当前事件活动的对象(捕获或者冒泡阶段)
        ev.currentTarget = ev.currentTarget || target;
        //取消冒泡的处理
        ev.stopPropagation = function(){
            if(stopPropagation){
                stopPropagation.call(event);
            }else{
                ev.cancelBubble = true;
            }
        };
        //取消默认事件的处理
        ev.preventDefault = function(){
            if(preventDefault){
                preventDefault.call(event);
            }else{
                ev.returnValue = false;
            }
        };

        //执行callback函数,并且this指向,同时用flag接收其返回值
        var flag = callback.call(target,ev);

        //处理flag接收到的返回着为false的情况
        if(flag === false){
            ev.stopPropagation();
            ev.preventDefault();
        }
    }

    //返回回调函数,方便用于事件解绑
    return handler;

}

补充对于匿名函数取绑的问题:

之前采用了return回调函数的方法,同时,在绑定函数时,用一个变量来存储回调函数,在解绑时再将变量传入,以达到解绑的目的。

我们来看一段如下的代码

var a = addEvent(el,"click", function(){});
removeEvent(el,"click",a);

这种方法,其实对于解绑来说,代码量是很少的。同时,也不需要在解绑的时候,再对代码进行修改,将匿名函数变成非匿名,然后在进行操作。当然,这种方法也有些缺陷,匿名函数并不会占用命名,而这种方案,始终是需要对变量名进行占用。因此,如果执意于要对于匿名函数进行解绑的话,可以考虑对匿名函数变为非匿名的转换。参考代码如下

//事件注册
function addEvent(target,eventName,callback,useCapture){

    //压缩函数的空格
    var fnStr = callback.toString().replace(/\s+/g,‘‘);

    if(!target[eventName+"event"]){
        target[eventName+"event"] = {};
    }

    //存储事件的函数到target[eventName+‘event‘][fnStr]中
    target[eventName+"event"][fnStr] = handler;

    useCapture = useCapture || false;

    //高设上的事件注册简单兼容
    if(target.addEventListener){
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){
        target.attachEvent("on"+eventName,handler);
    }else{
        target["on"+eventName] = handler;
    }

    //处理传入的参数ev
    function handler(event){
        //ie下的事件名需要window.event
        var ev = event || window.event,
                stopPropagation = ev.stopPropagation,
                preventDefault = ev.preventDefault;

        //获取触发事件的对象 ie下的ev.srcElement相当于其他浏览器下ev.target
        ev.target = ev.target || ev.srcElement;
        //获取当前事件活动的对象(捕获或者冒泡阶段)
        ev.currentTarget = ev.currentTarget || target;
        //取消冒泡的处理
        ev.stopPropagation = function(){
            if(stopPropagation){
                stopPropagation.call(event);
            }else{
                ev.cancelBubble = true;
            }
        };
        //取消默认事件的处理
        ev.preventDefault = function(){
            if(preventDefault){
                preventDefault.call(event);
            }else{
                ev.returnValue = false;
            }
        };

        //执行callback函数,并且this指向,同时用flag接收其返回值
        var flag = callback.call(target,ev);

        //处理flag接收到的返回着为false的情况
        if(flag === false){
            ev.stopPropagation();
            ev.preventDefault();
        }
    }
}

//事件取绑(匿名函数)
function removeEvent(target,eventName,callback,useCapture){
    //压缩空格
    var fnStr = callback.toString().replace(/\s+/g,‘‘),
            handler;

    if(!target[eventName+"event"]){
        return;
    }

    //获取到存储的函数
    handler = target[eventName+"event"][fnStr];
    useCapture = useCapture || false;

    if(target.removeEventListener){
        target.removeEventListener(eventName,handler,useCapture);
    }else if(target.detachEvent){
        target.detachEvent("on"+eventName,handler);
    }else{
        target["on"+eventName] = null;
    }
}

对于ie下执行顺序的问题:

很多情况下,我们对于事件绑定的顺序肯定是有要求的,因此不按照顺序的执行很多时候是我们所不想看到的,因此,我们需要对于回调的执行顺序进行一个处理。处理的思路也不算复杂。倘若在ie下,我们对于同一个事件做了多个回调,那么我们将对其进行判断,并将其合并到一个回调之中。简单来说,就是如下的这种思路

//对于el绑定了a和b两个回调
el.attachEvent("onclick",a)
el.attachEvent("onclick",b)

//对两个回调进行整合处理,然后让其按顺序执行
el.attachEvent("onclick",function(){
	a();
	b();
})

上面是对于思路的一种抽象,不过当真正开始具体执行的时候,其实是很复杂的。

简单来说,这种执行方案,就是将多个函数进行打包,然后丢到一个事件绑定中去执行,而如果采用addEventListener或者attachEvent直接进行绑定的话,无论如何处理,都很难达到只对事件绑定一次的目的。(起码用这两个函数,采取直接对元素进行绑定,我没想到什么很好的解决方案。)当然,这也并不是不能解决的,很早之前Dean Edwards大神的addEvent库就巧妙的解决了这个问题,他并没有采用流行的addeventListener/attachEvent方法,而是直接采用dom0事件系统对其进行了处理,巧妙的利用了dom0事件系统只能绑定一个事件的特性。现在很流行的jquery事件系统,很多思想也是参考的这个库中的思想。

因此对于执行顺序的处理,在上面的事件注册之中,并没有提及到。但是在之后的章节会进行提及。同时,这样的事件注册,对于需求不算复杂的页面,也算是足够了。

第一章也就到这里了……

时间: 2024-11-01 23:54:14

前端事件的相关文章

一个简单的前端事件框架

参考网上的一个资料,做下备注. <html> <head> <title>js event demo</title> <meta http-equiv="pragma" content="no-cache"> <meta http-equiv="cache-control" content="no-cache"> <meta http-equiv=&

前端iframe标签介绍及使用

使用的场景: (1) 有重复的板块内容显示的时候 # 后端如果是模板渲染方式(得到页面是通过继承的形式),可以换成iframe来请求直接获取子功能页面 iframe标签的作用: iframe标签可以实现html主页面嵌套html子页面,子页面可以是一个功能页面,在某些时候使用iframe非常的方便 # 所以如果是模板渲染方式的,前端就可以使用iframe标签节省一点网络带宽(传输的内容会少一些,并且主页面不会刷新,只是iframe在请求得到新的资源). 模板渲染和iframe的对比: (1) 子

开发经验(漫谈)

我没有快速学习的能力,我不得不在时间花费上非常谨慎.我希望尽可能地学习到有持久生命力的技能,即不会在几年内就过时的技术.只要占主导地位的计算模型体系不变,我们如今使用的数据结构与算法在未来也会以另外的形式继续适用,也会成为程序员职业生涯中一笔长期巨大的财富. 我要重新发明轮子:在实现一个简单东西的时候,与其去花时间精力调查有没有能用的轮子,以及哪个轮子最好用,可能还不如自己实现一套,而且还对代码聊熟于心, 项目管理: 1.管理者是时针,下属是分针,秒针自动找他校对,对于领导,起到看点和到点报时的

使用限制函数执行频率的函数代理

使用代理限制函数的调用频率 假设一个经典的CURD页面上,要做一个Ajax异步查询功能. 放一个查询按钮,点击查询,系统会到远程服务端请求数据,一秒之后返回查询结果. 很快,功能实现了! 但假如用户一秒内点击了三次查询,会发生什么? 为了解决这个问题,我们可能会在用户点击查询之后禁用查询按钮,或者在处理查询时上锁,返回结果后再把锁放开. 很好,做到这里,已足够日常使用. 这里只解决了一个问题:按钮的点击.而输入框的输入.选择框的变化.鼠标的移动.滚轮的滚动,这些事件触发频率高的问题怎么解决? 为

Windows Azure HandBook (2) Azure China提供的服务

<Windows Azure Platform 系列文章目录> 对于传统的自建数据中心,从底层的Network,Storage,Servers,Virtualization,中间层的OS,Middleware,Runtime,最上层的Application,Data,都需要企业进行管理.这就好比农村自建房. 对于公有云平台,一般分为三种类型: IaaS, PaaS和SaaS. Microsoft Azure平台属于IaaS和PaaS范畴. 1. IaaS 对于用户来说,底层的Network,

iwebshop关于按钮点击提示的系列代码操作流程

在开发中遇到一个需求,需要点击按钮的时候js判断一下是否选中如果没选中 就不能提交如果选中就执行控制器中的方法! 前端js代码如下解析: function changeAction(){// js按钮事件的方法    if($('input:checkbox[name="id[]"]:checked').length > 0){//input标签状态的判断       $('#form表单的id').attr('action','{url:/控制器/方法}');       $(

通过一个案例精通以太坊智能合约和Solidity

作者介绍 Silver CEO 星际区块链信息发展有限公司 项目组件 ??这个项目是一个构建在以太坊上的游戏,感谢这个团队给我们提供的案例:https://cryptozombies.io ??从功能的角度看,有如下脚本: zombiefactory.sol:定义zombie和生成zombie. zombiefeeding.sol:定义小猫接口,给zombie吃小猫. zombieattack.sol:zombie打架的功能. erc721.sol:ERC721代币的接口. ownable.so

ALV Tree

找相关的alv tree demo:se38 -> bcalv_tree* 1.创建对话屏幕 由于ALV没有专门实现的控件,需要先在对话屏幕100上增加一个用户自定义控件区域(Custom Control),名为CONTAINER1 2.demo源码 *&---------------------------------------------------------------------* *& Report YALV_TREE *&-------------------

Monaco Editor 使用入门

以前项目是用ace编辑器的,但是总有些不敬人意的地方.前端事件看见的VS Code编辑器Monaco Editor准备更换下,下面介绍一些使用中遇到的一点问题.代码提示 1.项目引用 import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; 项目中引用了editor.api.js,但是这个文件不包含一些默认的语言和插件,所以在使用的时候,还需要我们自己import import 'monaco-editor/esm/vs/