Javascript事件

事件及事件的三种模型

一、event简介

         什么是事件呢?直观的说就是网页上发生的事情,大部分是指用户的鼠标动作和键盘动作,如点击、移动鼠标、按下某个键。为什么说大部分呢,因为事件不单单只有这两部分,还有其他的例如document的load和unloaded。只不过我们更加关注的是用户的操作。事件被封装成一个event对象,包含了该事件发生时的所有相关信息(event的属性)以及可以对事件进行的操作(event的方法)。

         event长啥样呢,来直观的看一下,比如我点击页面上一个按钮,产生的event对象如下:

 

         可以看到是一个MouseEvent对象,包含了一系列属性,如鼠标点击的位置等。那么敲击键盘时产生的event对象和它一样吗?看看就知道:

    可以看到是一个KeyboardEvent对象,并且属性跟上面的也不太一样,如没有clientX/Y,那是理所当然的啦,敲键盘怎么能获取到鼠标的位置呢。

           若你有一点面向对象编程的基础,看到这两个类名应该会有所思考,MouseEvent、KeyboardEvent会不会是继承自一个叫Event的类呢?恭喜你猜对了,确实如此。来看一下,我在window.onload监听函数中打印出event对象如下:

    属性少了很多,毕竟是父类嘛。

二、event对象常用属性、方法

1. 事件定位相关属性

    这部分属性平时用的还是挺多的,所以得着重介绍。如果你细细看了MouseEvent对象里的属性,一定发现了有很多带X/Y的属性,它们都和事件的位置相关。具体包括:x/y、clientX/clientY、pageX/pageY、screenX/screenY、layerX/layerY、offsetX/offset 六对。有点乱了吧,一个点击事件能有多少位置啊?不要着急,其实并不复杂,之所以能有这么多是因为各浏览器厂商在版本更迭的时候产生了很多不一致。

    结论:

    x/y与clientX/clientY值一样,表示距浏览器可视区域(工具栏除外区域)左/上的距离;

    pageX/pageY,距页面左/上的距离,它与clientX/clientY的区别是不随滚动条的位置变化;

    screenX/screenY,距计算机显示器左/上的距离,拖动你的浏览器窗口位置可以看到变化;

    layerX/layerY与offsetX/offsetY值一样,表示距有定位属性的父元素左/上的距离。

    之所以有那么多值一样的情况,就是由于浏览器兼容的原因。那我们平时该如何使用呢?请看下面的表格,列出了各属性的浏览器支持情况。(+支持,-不支持)

    offsetX/offsetY:W3C- IE+ Firefox- Opera+ Safari+ chrome+

    x/y:W3C- IE+ Firefox- Opera+ Safari+ chrome+

    layerX/layerY:W3C- IE- Firefox+ Opera- Safari+ chrome+

    pageX/pageY:W3C- IE- Firefox+ Opera+ Safari+ chrome+

    clientX/clientY:W3C+ IE+ Firefox+ Opera+ Safari+ chrome+

    screenX/screenY:W3C+ IE+ Firefox+ Opera+ Safari+ chrome+​

        说明:该表摘自其他文章,我未做全部验证,但从最新版本的现代浏览器来看,这些属性貌似是都支持了,为了更好的兼容性,通常我们选择W3C支持的就可以了。

2.其他常用属性

    target:发生事件的节点;

    currentTarget:当前正在处理的事件的节点,在事件捕获或冒泡阶段;

      timeStamp:事件发生的时间,时间戳。

    bubbles:事件是否冒泡。

    cancelable:事件是否可以用preventDefault()方法来取消默认的动作;

    keyCode:按下的键的值;

3. event对象的方法

    event. preventDefault()//阻止元素默认的行为,如链接的跳转、表单的提交;

    event. stopPropagation()//阻止事件冒泡

    event.initEvent()//初始化新事件对象的属性,自定义事件会用,不常用

    event. stopImmediatePropagation()//可以阻止掉同一事件的其他优先级较低的侦听器的处理

三、事件的三种模型

           由于复杂的历史原因,事件模型是不统一的,当然作为前端开发人员这种事情已经见怪不怪了。尽管W3C已经制定了DOM2标准来规范事件的定义,但由于顽固的IE6、7、8存在,我们还是得清楚IE的那一套定义。那么来看看三种模型都有哪些吧。

1. 原始事件模型

    在原始事件模型中(也有说DOM0级),事件发生后没有传播的概念,没有事件流。事件发生,马上处理,完事,就这么简单。监听函数只是元素的一个属性值,通过指定元素的属性值来绑定监听器。书写方式有两种:

    ①   HTML代码中指定属性值:<input type=”button” onclick=”func1()” />

    ②   在js代码中指定属性值:document.getElementsByTagName(‘input’)[0].onclick = func1

    优点:所有浏览器都兼容

    缺点:1)逻辑与显示没有分离;2)相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的,如:a.onclick = func1; a.onclick = func2;将只会执行func2中的内容。3)无法通过事件的冒泡、委托等机制(后面系列会讲到)完成更多事情。

    在当前web程序模块化开发以及更加复杂的逻辑状况下,这种方式显然已经落伍了,所以在真正项目中不推荐使用,平时写点博客小例子啥的倒是可以,速度比较快。

2. IE事件模型

    在参考其他资料时,我有看到这样的一句话“IE不把该对象传入事件处理函数,由于在任意时刻只会存在一个事件,所以IE把它作为全局对象window的一个属性”,为求证其真伪,我用IE8执行了代码alert(window.event),结果弹出是null,说明该属性已经定义,只是值为null(与undefined不同)。我想难道这个全局对象的属性是在监听函数里才加的?于是执行下面代码:

    window.onload = function (){alert(window.event);}

    setTimeout(function(){alert(window.event);},2000);

    结果第一次弹出【object event】,两秒后弹出依然是null。由此可见IE是将event对象在处理函数中设为window的属性,一旦函数执行结束,便被置为null了。IE的事件模型只有两步,先执行元素的监听函数,然后事件沿着父节点一直冒泡到document。冒泡机制后面系列会讲,此处暂记。IE模型下的事件监听方式也挺独特,绑定监听函数的方法是:attachEvent( "eventType","handler"),其中evetType为事件的类型,如onclick,注意要加’on’。解除事件监听器的方法是 detachEvent("eventType","handler" )

    IE的事件模型已经可以解决原始模型的三个缺点,但其自己的缺点就是兼容性,只有IE系列浏览器才可以这样写。

3. DOM2事件模型

    此模型是W3C制定的标准模型,既然是标准,那大家都得按这个来,我们现在使用的现代浏览器(指IE6~8除外的浏览器)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:

    (1)capturing phase:事件捕获阶段。事件被从document一直向下传播到目标元素,在这过程中依次检查经过的节点是否注册了该事件的监听函数,若有则执行。

    (2)target phase:事件处理阶段。事件到达目标元素,执行目标元素的事件处理函数.

    (3)bubbling phase:事件冒泡阶段。事件从目标元素上升一直到达document,同样依次检查经过的节点是否注册了该事件的监听函数,有则执行。

    所有的事件类型都会经历captruing phase但是只有部分事件会经历bubbling phase阶段,例如submit事件就不会被冒泡。 

    你可能会有疑问,为什么是这个样子的呢?流程有点太多了吧?事情的缘由还得从网景公司与微软争霸开始说起。在W3C的规范还没有出生的时候,市场上已经有两家强劲的浏览器厂商,产品分别是微软的IE和网景的Netspace Navigator(后面简称NN),IE的事件模型上面已介绍,事件是可以冒泡的。然而NN却不这么认为,它的模型中,事件是从上往下走的,即只有捕获阶段。两家都没有谁对谁错,因为按照他们的模型都可以完成事件的处理。然后W3C珊珊来迟,要制定标准,要统一,所以也就只能两家的都采纳,谁也不得罪,然后用标准制定者的口吻宣布:W3C模型工作良好。从此天下太平。

    说远了,赶紧来看看标准的事件监听器该如何绑定:addEventListener("eventType","handler","true|false");其中eventType指事件类型,注意不要加‘on’前缀,与IE下不同。第二个参数是处理函数,第三个即用来指定是否在捕获阶段进行处理,一般设为false来与IE保持一致,除非你有特殊的逻辑需求。监听器的解除也类似:removeEventListner("eventType","handler","true!false");

        以上便是事件的三种模型,我们在开发的时候需要兼顾IE与非IE浏览器,所以注册一个监听器应该这样写:

var a = document.getElementById('a');
if(a.attachEvent){
    a.attachEvent('onclick',func);
}
else{
    a.addEventListener('click',func,false);
}

        感觉很麻烦吧?因此我们一般会借助现有框架或类库已经封装好的,比如jQuery,后面将会介绍jQuery中强大的事件监听方式。

事件的捕获-冒泡机制及事件委托机制

一、事件的捕获与冒泡

   由W3C规定的DOM2标准中,一次事件的完整过程包括三步:捕获→执行目标元素的监听函数→冒泡,在捕获和冒泡阶段,会依次检查途径的每个节点,如果该节点注册了相应的监听函数,则执行监听函数。以下面的HTML结构为例:

<div id="parentdiv">
       父亲
       <div id="childdiv">孩子</div>
</div>

  执行的流程应该是这样的:

  下面是一组例子,分别点击孩子节点可以清楚的看到第三个参数的影响:

父亲
孩子

 

父亲节点的监听函数在捕获阶段执行:
var parent1 = document.getElementById('parentdiv1');
var child1 = document.getElementById('childdiv1');
parent.addEventListener('click',function(){alert('父亲被点击了');},true);//第三个参数为true
child.addEventListener('click',function(){alert('孩子被点击了');},false);

 

父亲
孩子

 

父亲节点的监听函数在冒泡阶段执行:
var parent2 = document.getElementById('parentdiv2');
var child2 = document.getElementById('childdiv2');
parent.addEventListener('click',function(){alert('父亲被点击了');},false);//第三个参数为false
child.addEventListener('click',function(){alert('孩子被点击了');},false);

 

父亲
孩子

 

父亲节点的监听函数在捕获冒泡阶段都执行:
var parent3 = document.getElementById('parentdiv3');
var child3 = document.getElementById('childdiv3');
parent.addEventListener('click',function(){alert('父亲被点击了');},true);//第三个参数为true
parent.addEventListener('click',function(){alert('父亲被点击了');},false);//第三个参数为false
child.addEventListener('click',function(){alert('孩子被点击了');},false);

  如果不想让事件向上冒泡,可以在监听函数中调用event.stopPrapagation()来完成,这样父亲节点就捕捉不到该事件了。在实际的开发中,这一用处还是挺多的。

二、事件委托机制

       知道了事件的捕获冒泡机制,我们可以利用它来实现更方便的程序控制,事件委托便是最典型的应用之一。下面来说说javascript中的事件委托机制。什么叫委托呢?想想我们现实生活中,自己不想干的事,让别人来帮忙完成,这就是把事情“委托”给别人。Javascript的事件委托机制也是这个道理,本来一个监听函数要处理节点a触发的事件,现在把这个监听函数绑定到节点a的父层节点上,让它的父辈来完成事件的监听,这样就把事情“委托”了过去。在父辈元素的监听函数中,可通过event.target属性拿到触发事件的原始元素,然后再对其进行相关处理。

       那这样做有什么好处呢?最大的用处便是监听动态增加的元素。比如我们现在有这样的需求,点击下面每个列表项弹出各自的内容,现在随着web应用的盛行,网页中使用异步请求来动态加载节点已经变的很普遍,所以我们点击下方的按钮要在列表中增加一项,并且点击新增加的节点也要弹出内容。HTML结构如下:

<ol id="olist">
    <li>列表内容1</li>
    <li>列表内容2</li>
    <li>列表内容3</li>
    <li>列表内容4</li>
    <li>列表内容5</li>
</ol>

  若我们使用之前的监听器绑定方式,需要遍历所有的li元素并监听,代码应该是这样的:

var listArray = document.getElementById('olist').childNodes;
for(var i=0;i<listArray.length;i++){
    listArray[i].addEventListener('click',function({
            alert(this.innerText);
        });
}

  运行效果如下:

  1. 列表内容1
  2. 列表内容2
  3. 列表内容3
  4. 列表内容4
  5. 列表内容5

  可以发现当新增元素后,点击它并没有弹出内容。那是当然的了,因为我们并没有给新增的元素绑定监听器,为了实现点击新增元素也弹出内容,我们不得不在每次新增一个元素后,再进行一次绑定。加一个绑一个,加一个绑一个,累不累啊!你不累浏览器都累了!这样做导致的性能开销是可想而知的,而且浏览器还要维系n多元素与应的监听函数的映射关系,会占用大量内存。

       面对这样拖沓冗杂的代码,你是不是已经不能忍,想要高喊一声:大地!快使用光能力量!好,接下来该秘密武器登场了,看看使用事件委托的效果,代码如下:

var olist = document.getElementById('olist');
olist.addEventListener('click',function(){
    alert(event.target.innerText);
},false);

  看看实际运行的效果:

  1. 列表内容1
  2. 列表内容2
  3. 列表内容3
  4. 列表内容4
  5. 列表内容5

  我们并未给li元素绑定任何监听器,而是监听它的父元素ul,等到事件冒泡上来的时候,在处理函数中通过event.target获得触发事件的li元素,进行相关处理。这样做的好处是显而易见的,首先只进行了一次监听器的绑定,浏览器轻松,其次动态增加元素后你也不必要再绑定监听器,你也轻松。正所谓大家好才是真的好!

       本篇的基本内容就介绍完了,你是不是感觉有点奇妙,我平时写程序的时候没关心这些也仍然能完成工作呀?那我就得问你是不是使用js框架,使用jQuery了,事实上,jQuery提供的on、live等方法就已经对事件委托进行了封装,为委托机制的推广悄悄做了底层贡献,你没感觉到而已。jQuery中的各种事件监听方式也需要我们有一个清楚的了解,才能正确的使用,高效的完成工作。这些内容将放在下一篇介绍。

jQuery中的事件监听方式及异同点

jQuery中的四种事件监听方式

  jQuery中提供了四种事件监听方式,分别是bind、live、delegate、on,对应的解除监听的函数分别是unbind、die、undelegate、off。在开始看他们之前,先来声明一个例子,各函数的用法将围绕这个例子进行,html代码如下:

<ol id="myol">
    <li>列表元素1</li>
    <li>列表元素2</li>
    <li>列表元素3</li>
    <li>列表元素4</li>
</ol>

  同时再声明一个函数,用来作为监听函数,JS代码如下:

function getHtml(){
    alert(this.innerHTML);
}

  看完例子大家应该明白想要干什么了,没错,就是实现点击每个列表元素的时候,把它的内部html弹出来,灰常简单~

  忍不了了,奔主题奔主题!下面来分别看一下这四种方式:

bind(type,[data],function(eventObject))

  在我初学jQuery的时候,这个函数用的是最多的(基本上就认它),作用就是在选择到的元素上绑定特定事件类型的监听函数,参数的含义如下:

  type:事件类型,如click、change、mouseover等;

  data:传入监听函数的参数,通过event.data取到。可选;

  function:监听函数,可传入event对象,这里的event是jQuery封装的event对象,与原生的event对象有区别,使用时需要注意。

  来看看bind的源码:

bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    }

  可以看到内部是调用了on方法,这个on是什么样的呢?稍后我们再看。先用我们上面的例子来试试:

$('#myol li').bind('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  bind的特点就是会把监听器绑定到目标元素上,有一个绑一个,在页面上的元素不会动态添加的时候使用它没什么问题。但如果列表中动态增加一个“列表元素5”,点击它是没有反应的,必须再bind一次才行。要想不这么麻烦,我们可以使用live。jQuery还有一种事件绑定的简写方式如a.click(function(){});、a.change(function(){});等,它们的作用与bind一样,仅仅是简写而已。

live(type, [data], fn)

  参数与bind一样,源码是怎样的呢?

live: function( types, data, fn ) {
        jQuery( this.context ).on( types, this.selector, data, fn );
        return this;
    }

  可以看到live方法并没有将监听器绑定到自己(this)身上,而是绑定到了this.context上了。这个context是什么东西呢?其实就是元素的限定范围,看了下面的代码就清楚了:

$('#myol li').context; //document
$('#myol li','#myol').context; //document
$('#myol li',$('#myol')[0]); //ol 

  通常情况下,我们都不会像第三种方式那样使用选择器,所以也就认为这个context通常就是document了,即live方法把监听器绑定到了document上了。不把监听器直接绑定在元素上,你是不是想起事件委托机制来了呢?live正是利用了事件委托机制来完成事件的监听处理,把节点的处理委托给了document。在监听函数中,我们可以用event.currentTarget来获取到当前捕捉到事件的节点。下面的例子来揭晓:

$('#myol li').live('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  使用事件委托的优点一目了然,新添加的元素不必再绑定一次监听器。看来live这货还真不错,以后抛弃bind就用它了!可以吗?答案是否定的,而且是大大的否定。因为将监听器绑定到了document上,所以事件的处理得等待层层冒泡,直到冒泡到根节点才开始处理,在DOM树较深或者节点的嵌套关系很复杂时,会有意想不到的结果,根节点的负担太重了。就像四世同堂、五世同堂,甚至八世同堂(现实中不太可能,但在HTML中层级关系可能远比这还多),老爷子肯定记不清哪个孙子是哪个儿子的,哪个重孙又是哪个儿子的儿子的,老爷子脑子一乱,糊涂了,事情就办错了。为此,jQuery官方已宣布在1.7版本开始废弃live,改用其他方式代替。所以我们也顺应号召,罢用此方法。

  正因为live存在那样的缺点,所以我们就思考,既然老爷子负担那么重,可不可以别把监听器绑定在document上呢,绑定在就近的父级元素上不就好了。顺应正常逻辑,delegate诞生了。

delegate(selector,type,[data],fn)

  参数多了一个selector,用来指定触发事件的目标元素,监听器将被绑定在调用此方法的元素上。看看源码:

delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    }

  又是调用了on,并且把selector传给了on。看来这个on真的是举足轻重的东西。照样先不管它。看看示例先:

$('#myol').delegate('li','click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  我们在例子中将监听器绑定到ol上,event.currentTarget显示当前捕获到事件的元素是ol。这下,我们的选择又多了一些灵活性,不单可以利用事件委托,还可以选择委托的对象。毕竟老麻烦同一个人帮忙很不好嘛。对于如何选择委托对象,还是需要一定的策略的,毕竟父级元素可以有很多。我觉得原则应该是选择最近的“稳定”元素,选择最近是因为事件可以更快的冒泡上去,能够在第一时间进行处理。所谓“稳定”是指该父级元素是一开始就在页面上的,不是动态添加上来的,而且将来也不会消失掉,这样可以保证它可以时时监控着自己的孩子。

  看了这么多,你是不是迫不及待想看看这个on的真实面目了呢,这就来:

on(type,[selector],[data],fn)

  参数与delegate差不多但还是有细微的差别,首先type与selector换位置了,其次selector变为了可选项。交换位置的原因不好查证,应该是为了让视觉上更舒服一些吧。至于selector为什么是可选了呢,速度回想。。。对了,bind也调用了它,因为bind是绑定在了自己身上,所以只能传个null进来。对吗?不对,传null和不传是两回事啊,差点掉坑里。看看on的源码里能不能找到线索呢,打开源码只发现一句关键的:

return this.each( function() {
            jQuery.event.add( this, types, fn, data, selector );
        })

  在on的内部,又调用了event.add方法,纳尼?顺着再走一步好了,进入event.add查看,越加复杂,不过还好,看到了一段熟悉的代码:

if ( elem.addEventListener ) {
     elem.addEventListener( type, eventHandle, false );
     } else if ( elem.attachEvent ) {
         elem.attachEvent( "on" + type, eventHandle );
        }

  看来已经开始进行事件监听,不会再往深走了,舒一口气继续往下看:

// Add to the element's handler list, delegates in front
if ( selector ) {
     handlers.splice( handlers.delegateCount++, 0, handleObj );
     } else {
       handlers.push( handleObj );
       }

  传不传selector的区别就在此处了!但是!区别到底是什么啊,智商捉急!翻来覆去就是看不明白。哎,本来想通过源码把事情说明白的,可惜在下不才,还是献丑了。暂时还是说不明白了。等待日后更新此处吧。

         不过,解释不明白源码还是可以通过例子来理解嘛!我们先不传selector来看看:

$('#myol li').on('click',getHtml);
  1. 列表元素1
  2. 列表元素2
  3. 列表元素3
  4. 列表元素4

  可以看到event.currentTarget是li自己,与bind的效果一样。我为什么一直要纠结这个传不传selector的区别呢?老老实实传进去不就完了。其实是因为之前有看过文章提到如何用on来代替bind和live的写法,其中代替bind的写法就是不传selector进去,今日就想探清楚这个究竟。至于传selector进去,就是跟delegate一样的意义了,除了参数顺序不同,其他完全一样。

  终于看到on的真实作用了,那么,这么多的事件绑定方式,我们该如何进行选择呢?

bind、live、delegate、on如何选择?

  其实这个问题是完全不必纠结的,因为你已经知道他们之间的区别了不是么?根据实际情况斟酌使用就行。不过官方有一个推荐就是尽量使用on,因为其他方法都是内部调用on来完成的,直接使用on可以提高效率,而且你完全可以用on来代替其他三种写法。至于如何代替我想就不必这么直白的写出来了,真正理解它们的区别之后自然而然也就不是难事了。

总结

  jQuery的事件监听方式就分析完了,除了后面看event.add源码有点水分之外,应该是能把整个原理解释明白了。

 

备注:

      阮一峰:《JavaScript 标准参考教程》Event对象部分

posted @   掉进书洞里的猫  阅读(271)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述
点击右上角即可分享
微信分享提示