事件模型

事件是一种实现异步编程的实现方式,本质上是程序各个组件部分之间的通信。DOM支持大量的事件。

EventTarget接口

DOM的事件操作(监听和触发),都定义在EventTarget接口。Element节点、document节点和window对象,都部署了这个接口。此外,SMLHttpRequest、AudioNode、AudioContentext等浏览器内置对象,也部署了这个接口。

该接口就是三个方法。

addEventListener:绑定事件的监听函数
removeEventListener:移除事件监听的函数
dispathEvent:触发事件

addEventListener()

addEventListener方法用于在当前节点或者对象上,定义一个特定事件监听函数。

target.addEventListener(type,listener[,useCapture]);

window.addEventListener('load',function(){...},false);
request.addEventListener('readystatechange',function(){...},false);

type:事件名称,大小写铭感。
listener:监听函数。事件发生时,会调用该监听函数。
useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发,默认为false(监听函数只在冒泡阶段被触发)。

function hello(){
    console.log('Hello world');
}

上面代码中,addEventListener方法为button元素节点,绑定click事件的监听函数hello,该函数只在冒泡阶段触发。

addEventListenter方法可以作为当前对象的同一个事件。添加诸多的监听函数,这些函数按照添加顺序触发,即先添加先触发。如果同一个事件多次添加同一个监听函数,该函数只执行一次,多余的添加将会自动被去除(不必使用removeEventListener方法手动去除)。

function hello(){
    console.log('Hello Wolrd');
}
document.addEventListenter('click',hello,false);
document.addEventListenter('click',hello,false);

执行上面文档,点击文档只会输出一行Hello World。

如果希望向监听函数传递参数,可以使用匿名函数包装一下监听函数。

function print(x){
    console.log(x);
}

var el = document.getElementById('div1');
el.addEventListener('click',function(){print('Hello');},false);

上面代码通过匿名函数,向监听函数传递了一个参数。

removeEventListener()

removeEventListener方法用来移除addEventListener方法添加的事件监听函数。

div.addEventListener('click',listener,false);
div.removeEventListener('click',listener,false);

removeEventListener方法的参数,与addEventListener方法完全一致。它的第一个参数”事件类型”大小写铭感。

注意,removeEventListener方法移除的监听函数,必须与对应的addEventListener方法参数完全一致,而且必须在同一个元素节点,否则无效。

dispatchEvent()

dispatchEvent方法在当前节点上触发执行事件,从而触发监听函数执行。该方法赶回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回为false,否则为true。

target.dispatchEvent(event);

dispathEvent方法的参数是一个Event对象的实例。

para.addEventListener('click',hello,false);
var event = new Event('click');
para.dispatchEvent(event);

上面代码在当前节点触发了click事件。

如果dispathEvent方法的参数为空,或者不是一个有效的事件对象,就报错。

下面代码根据dispathEvent方法的返回值,判读事件是否被取消。

var canceled = !cb.dispatchEvent(event);
if(canceled){
    console.log('事件取消');
}else{
    console.log('事件未取消');
}

监听函数

事件监听函数(listener)是事件发生时,程序要执行的函数。它是事件驱动编程的主要编程方式。

DOM提供三种方式,可以为事件绑定监听函数。

HTML标签的On-属性

HTML语言允许在元素标签中,直接定义某些事件的监听代码。

<body onload = "doSometing()">
<div onclick = "console.log('触发事件')">

上面的代码为body节点的load事件,div节点的click事件,指定了监听函数。

使用这种方式指定的监听函数,只会在冒泡阶段触发。

注意,使用这种方式时,on-属性值将会执行代码,而不是一个函数。

<!-- 正确方式 -->
<body onload = "donSometing()">

<!-- 错误方式 -->
<body onload = "doSomething">

一旦指定事件发生。on-属性值是按原样传入JavaScript引擎执行。因此如果要执行函数,不要忘记加上一对小括号。

另外,Element元素节点的setAttribute方法,其实设置的也是这种效果。

el.setAttribute('onclick','doSometing()');

Element节点的事件属性

Element节点对象有事件属性,同样可以指定监听函数。

window.onload = doSometing;
div.onClick = function(event){
    console.log('触发事件');
}

使用这个方法可以指定监听函数,只会在冒泡阶段触发。

addEventListener方法

通过Element接单、document节点、window对象的addEventListener方法,也可以定义事件的监听函数。

window.addEventListener('load',doSometing,false);

在上面的三种方法中,第一种”HTML标签的on-属性”,违反了HTML与JavaScript代码相分离的原则;第二种”Element节点的时间属性”的确定是,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次会覆盖前一次。因此,这两种方法都不推荐使用,除非是为了程序的兼容性问题。因为所有浏览器都支持这两种方法。

addEventListener是推荐的指定监听函数的方法,它有如下优点:

可以针对同一个事件,添加多个监听函数。
能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发会监听函数。
除了DOM节点,还可以部署在window、XMLHttpRequest等对象上面,等于统一了整个JavaScript的监听函数接口。

this对象的指定

实际编程中,监听函数内部的this对象,常常需要指向触发事件的那个Element节点。

addEventListener方法指定的监听函数,内部的this对象总是指向触发事件那个点。

<p id="para">Hello</p>
var id = 'doc';
var para = document.getElementById('para');

function hello(){
    console.log(this.id);
}
para.addEventListener('click',hello,false);

执行上面代码,点击<p>节点会输出para。这是因为监听函数拷贝函数被”拷贝”成节点的一个属性,所以this指向节点对象。使用下面的方法,会看的很清楚。

para.onclick = hello;

如果监听函数部署在Element节点的on-属性上面,this不会执行触发事件的元素节点。

<p id="para" onclick="hello()">Hello</p>

执行上面代码,点击<p>节点会输出doc。这是因为这里只调用了hello函数,而hello函数实际上是在全局作用域执行,相当于下面的代码。

para.onclick = function(){
    hello();
}

一种解决办法是,不引入函数作用域,直接在on-属性写入要执行的代码。因为on-属性是在当前节点上执行的。

<p id="para" onclick = "console.log(id)">Hello</p>
<p id="para" onclick = "console.log(this.id)">Hello</p>

上面两行数据,最后输出的都是para。

总结一下,一下写法的this对象都会指向Element节点。

element.onclick = print;
element.addEventListener('click',print,false);
element.onclick = function(){console.log(this.id);}

<element onclick="doSomething()">

时间传播的三个阶段

当一个事件发生以后,它会在不同的DOM节点之间传播(propagation)。这种传播分成三个阶段:

第一阶段 : 存window对象传播到目标节点,称为”捕获阶段”(capture phase)。
第二阶段:在目标节点触发,称为“目标阶段”(target phase)。
第三阶段:从目标节点传导回来window对象,称为”冒泡阶段”(bubbing phase)。

这种三阶段的传播型,会使得一个事件在多个节点触发。比如,假设点击<div> 之中嵌套一个<p> 节点。

<div>
    <p>Click Me</p>
</p>

如果对这两个节点的click时间都设置监听函数,则click时间会被触发四次。

var phases = {
    1:'capture',
    2:'terget',
    3:'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');

div.addEventListener('click',callback,true);
p.addEventListener('click',callback,true);
div.addEventListener('click',callback,false);
p.addEventListener('click',callback,false);

function callback(event){
    var tag = event.currentTarget.tagName;
    var phase = phase[event.eventPhase];
    console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}

上面代码表示,click时间被触发了四次:<p> 节点的捕获阶段和冒泡阶段各1次,<div> 节点的捕获阶段和冒泡阶段各一次。

1.捕获阶段:事件<div><p> 传播,触发<div> 的click事件;
2.目标阶段:事件从<div> 到达<p> 时,触发<p> 的click事件;
3.目标阶段:事件离开<p> 时候,触发<p> 的click事件;
4.冒泡阶段:事件从<p> 传回<div> 时,再次触发<div> 的click事件。

当用户点击网页时,浏览器总会指定click事件的目标节点,就是点击位置嵌套最深的那个节点。所以<p> 节点和的捕获阶段和冒泡阶段,都会显示target阶段。

事件传播的最上层对象是window,接着依次是document、html(document.documentElement)、body(document.body)。也就是说body元素中有一个div元素,点击该元素。事件的传播顺序,在捕获阶段依次为window、document、html、body、div,在捕获阶段依次为div、body、html、document、window。

事件的代理

由于事件在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方法叫做事件的代理(delegation)。

var ul = document.querySelector('ul');
ul.addEventListener('click',function(event){
    if(event.target.tagName.toLowerCase() === 'li'){
        ...
    }
})

上面代码的click时间的监听函数定义在<ul> 上,但实际上,它处理的是子节点

  • 的click事件。这样的好处是,只要定义一个事件监听函数,就能处理多个子节点的事件,而且以后添加子节点,监听函数仍然有效。
  • 如果希望到某个节点为止,不再传播,可以使用事件对象的stopPropagation 方法。

    p.addEventListener('click',function(event){
        event.stopPropagation()'
    })

    使用上面的代码以后,click事件只在冒泡阶段到达

    节点以后,就不在向上(父节点方向)传播了。

    但是,stopPropagation方法只会阻止当前监听函数的传播,不会阻止p节点上其他click事件的监听函数。如果想要不再触发那些监听函数,可以使用stopImmediatePropagation方法。

    p.addEventListener('click',function(event){
        event.stopImmediatePropagation();
    });
    
    p.addEventListener('click',function(event){
    
    });

    Event对象

    事件发生以后,会生成一个事件对象,作为参数传给监听函数。浏览器提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。

    Event对象本身就是一个构造函数,可以用来生成新的实例。

    event = new Event(typeArg,eventInit);

    Event构造函数接受两个参数。第一个参数是字符串,表示事件的名称;第二个参数是一个对象,表示事件的配置。该参数可以有以下两个属性。

    bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。
    cancelable:布尔值,可选,默认为false,表示事件是否可以被取消。

    var ev = new Event(
        'look',
        {
            'bubbles':true,
            'cancelable':false
        }
    );
    document.dispatchEvent(ev);

    上面代码创建一个look事件实例,然后使用dispatchEvent方法触发该事件。

    IE8以下版本,事件对象不作为参数传递,而是通过window对象的event属性读取,并且实践对象的terget属性叫做srcEvent属性,所以获取时间信息,往往要写成下面的样子。

    function myEventHandler(event){
        var actualEvent = event || window.event;
        var actualTarget = actualEvent.target || actualEvent.srcElement;
    }
    // ...

    event.bubbles,event.eventPhase

    以下属性与事件的阶段相关。

    bubbles

    bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,只能在新建事件的时候改变。除非显式声明,Event函数生成的事件,默认不冒泡的。

    function goInput(e){
        if(!e.bubbles){
            passItOn(e);
        }else{
            doOutput(e);
        }
    }

    上面代码根据事件是否冒泡,调用不同的函数。

    event.eventPhase

    eventPhase属性返回一个整数值,表示事件目前所处的阶段。

    var phase = event.eventPhase;

    0,事件目前没有发生。
    1,事件目前处于捕获阶段。
    2,事件到达目标节点。
    3,事件处于冒泡阶段。

    event.cancelable,event.defaultPrevented

    以下属性与时间的默认行为有关。

    cancelable

    cancelable属性返回布尔值,表示事件是否可以取消。该属性为只读属性,只能在新建事件时改变。除非显式声明,Event构造函数生成的事件,默认是不可取消的。

    var bool = event.cancelable;

    如果要取消某个事件,需要在事件上调用preventDefault方法,这会阻止浏览器对某种事件部署的默认行为。

    defaultPrevented

    defaultPrevented属性返回一个布尔值,表示该事件是否调用过preventDefault方法。

    if(e.defaultPrevented){
        // ...  
    }

    event.currentTarget,event.target

    以下属性与事件的目标节点有关。

    currentTarget

    currentTarget属性返回当前事件当前所在的节点,即正在执行的监听函数所绑定的那个节点。作为比较,target属性返回当前事件发生的节点。如果监听函数在捕获阶段和冒泡阶段触发,那么这两个属性返回的值是不一样的。

    function hide(e){
        console.log(this === e.currentTarget);
        e.currentTarget.style.visibility = "hidden";
    }
    para.addEventListener('click',hide,false);

    上面代码中,点击para节点,该节点会不可见。另外,在监听函数中,currentTarget属性实际上等同于this对象。

    target

    target属性返回触发事件的那个节点,即事件最初发生的节点。如果监听函数不在该节点触发,那么它与currentTarget属性返回的值不一样。

    function hide(e){
        console.log(this === e.target);
        e.target.style.visibility = "hidden";
    }
    
    //<p id = "para">Hello <em>World</em></p>
    para.addEventListener('click',hide,false);

    上面代码中,如果在para节点的em子节点上点击,则e.target指向em子节点,导致em子节点(即world部分)会不可见,且输出false。

    在IE6-IE8之中,该属性的名字不是target,而是srcElement,因此经常可以看到下面这样的代码。

    function hide(e){
        var target = e.target || e.srcElement;
        target.style.visibility = "hidden";
    }

    event.type,event.detail.event.timeStamp,event.isTrusted

    以下属性与事件对象的其他信息相关。

    type

    type属性返回一个字符串,表示事件类型,大小敏感。

    var string = event.target;

    detail

    detail属性返回一个值,表示当前事件的某种信息。具体含义与事件类型有关,对于鼠标事件,表示鼠标事件在某个位置按下的次数,比如对于dbclick事件,detail属性的值总是2.

    function giveDetails(e){
        this.textContent = e.detail;
    }
    el.onclick = giveDetails;

    timeStamp

    timeStamp属性返回一个毫秒的时间戳,表示事件发生的时间。

    var number = event.timeStamp;

    下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。

    var previousX;
    var previousY;
    var previousT;
    
    window.addEventListener('mousemove',function(event){
        if(!(previousX === undefined || previousY === undefined || previousT === undefined)){
            var deltaX = event.screenX - previousX;
            var deltaY = event.screenY - previousY;
            var deltaD = Math.sqrt(Math.pow(deataX,2) + Math.pow(deataY,2));
            var deltaT = event.timeStamp - previousT;
            console.log(deltaD / deltaT * 1000);
        }
        previousX = event.screenX;
        previousY = event.screenY;
        previousT = event.timeStamp;
    })

    isTrusted

    isTrusted属性返回一个布尔值,表示该事件是否为真实用户触发。

    var bool = event.isTruseted;

    用户触发的时间返回true,脚本触发的事件返回false。

    event.preventDefault()

    preventDefault方法取消浏览器对当前事件的默认行为,比如点击链接后,浏览器跳转到指定页面,或者按下一个空格键,页面向下滚动一段距离。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,则调用该方法不会有任何的效果。

    该方法不会阻止事件的进一步传播。只要在事件的传播过程中,使用了preventDefault方法,该事件的默认发就不会执行。

    <input type="checkbox" id="my-checkbox" />
    var cb = document.getElementById('my-checkbox');
    cb.addEventListener(
        'click',
        function(e){e.preventDefault();},
        false
    );

    上面代码为点击单选框的事件,设置监听函数,取消默认行为。由于浏览器的默认行为是选中单选框,所以这段代码就会导致无法选中单选框。

    利用这个方法可以为文本框设置检验条件。如果用户输入不符合条件,就无法将字符输入文本框。

    function checkBox(e){
        if(e.charCode < 97 || e.charCode > 122){
            e.preventDefault();
        }
    }

    上面函数设为文本框的keypress监听函数以后,将智能输入小写字母,否则输入事件的默认行为将会被取消。

    event.stopPropagation()

    stopPropagation方法阻止事件在DOM中继续传播,防止再触发定义在别的节点上的监听事件,但是不包括当前节点上新定义的事件监听函数。

    function stopEvent(e){
        e.stopPropagation();
    }
    el.addEventListener('click',stopEvent,false);

    将上面函数指定为监听函数,会阻止事件进一步冒泡到el节点的父节点。

    event.stopImmediatePropagation()

    stopImmediatePropagation方法会阻止同一个事件的其他监听函数被调用。

    如果同一个节点对同一个事件指定多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了stopImmediatePropagation方法,其他函数就不会再执行了。

    function l1(){
        e.stopImmediatePropagetion();
    }
    function l2(){
        console.log('hello,world');
    }
    el.addEventListener('click',l1,false);
    el.addEventListener('click',l2,false);

    自定义事件和事件模拟

    除了浏览器定义的那些事件,用户还可以自定义事件,然后手动触发。

    var event = new Event('build');
    
    elem.addEventListener('build',function(e){...},false);
    
    elem.dispatchEvent(event);

    上面代码触发了自定义事件,该事件会层层向上冒泡。在冒泡的过程中,如果有一个元素定义了该事件的监听函数,该该监听函数就会触发。

    IE不支持这个API。

    CustomEvent()

    Event函数只能构造函数只能指定事件名,不能在事件上绑定数据。如果需要在出发事件的同时,传入指定的数据,需要使用CustomEvent构造函数生成自定义的事件对象。

    var event = new CustomEvent('build',{'detail':'hello'});
    function eventHandler(e){
        console.log(e.detail);
    }

    上面代码中,CustomEvent构造函数的第一个参数是事件的名称,第二个参数是一个对象,该对象的detail属性会绑定在事件对象之上。

    下面是一个例子。

    var myEvent = new CustomEvent("myevent",{
        detail:{
            foo:"bar",
        },
        bubbles:true,
        cancelable:false
    });
    
    el.addEventListener('myevent',function(event){
        console.log('Hello ' + event.detail.foo);
    });
    
    el.dispatchEvent(myEvent);

    IE不支持这个写法,可以用下面的函数模拟。

    (function(){
        function CustomEvent(event,params){
            params = params || {bubbles:false,cancelable:false,detail:undefined};
            var evt = document.createEvent('CustomEvent');
            evt.initCustomEvent(event,params.bubbles,params.cancelable,params.detail);
            return evt;
        }
        CustomEvent.prototype = window.Event.prototype;
        window.CustomEvent = CustomEvent;
    })();

    事件的模拟

    有时候,需要在脚本中摸种类型的事件,这是必须使用这种事件的构造函数。

    下面是一个通过MouseEvent构造函数,模拟触发click鼠标事件的例子。

    function simulateClick(){
        var event = new MouseEvent('click',{
            'bubbles':true,
            'cancelable':true
        });
        var cb = document.getElementById('checkbox');
        cb.dispatchEvent(event);
    } 

    自定义事件的老式写法

    老式浏览器不一定支持各类事件的构造函数。因此,有时候为了兼容,会用到一下非标准的写法。

    document.createEvent()

    document.createEvent()方法用来新建指定类型的事件。它所生成的Event实例,可以传入dispatchEvent方法。

    // 新建Event实例
    var event = document.createEvent('Event');
    
    // 事件的初始化
    event.initEvent('build', true, true);
    
    // 加上监听函数
    document.addEventListener('build', doSomething, false);
    
    // 触发事件
    document.dispatchEvent(event);

    createEvent方法接受一个字符串作为参数,可能的值参见下面。

    事件类型事件初始化方法
    UIEventsevent.initUIEvent
    MouseEventsevent.initMutationEvent
    HTMLEventsevent.initEvent
    Eventevent.initEvent
    CustomEventevent.initCustomEvent
    KeyboardEventevent.initCustomEvent

    event.initEvent

    事件对象initEvent方法。用来初始化事件对象,还能对事件对象添加属性。该方法的参数必须是一个使用Document.createEvent()生成的Event实例,而且必须在dispatchEvent方法之前调用。

    var event = document.createEvent('Event');
    event.initEvent('my-custom-event', true, true, {foo:'bar'});
    someElement.dispatchEvent(event);

    initEvent方法可以接受四个参数。

    type:事件名称,格式为字符串。
    bubbles:事件是否应该冒泡,格式为布尔值。可以使用event.bubbles属性读取它的值。
    cancelable:事件是否能被取消,格式为布尔值。可以使用event.cancelable属性读取它的值。
    option:为事件对象指定额外的属性。

    事件模拟的来势写法

    事件模拟的非标准做法是,对document.createEvent方法生成的事件对象,使用对应的事件初始化方法进行初始化。比如,click事件对象属于MouseEvent对象,也属于UIEvent对象,因此要用initMouseEvent方法或initUIEvent方法进行初始化。

    event.initMouseEvent()

    initMouseEvent方法用来初始化Document.createEvent方法新建的鼠标事件。该方法必须在事件新建(document.createEvent方法)之后、触发(dispatchEvent方法)之前调用。

    initMouseEvent方法有很长的参数。

    event.initMouseEvent(type, canBubble, cancelable, view,
      detail, screenX, screenY, clientX, clientY,
      ctrlKey, altKey, shiftKey, metaKey,
      button, relatedTarget
    );

    上面这些参数的含义,参见MouseEvent构造函数的部分。

    模仿并触发click事件的写法如下。

    var simulateDivClick = document.createEvent('MouseEvents');
    
    simulateDivClick.initMouseEvent('click',true,true,
      document.defaultView,0,0,0,0,0,false,
      false,false,0,null,null
    );
    
    divElement.dispatchEvent(simulateDivClick);

    UIEvent.initUIEvent()

    UIEvent.initUIEvent()用来初始化一个UI事件。该方法必须在事件新建(document.createEvent方法)之后、触发(dispatchEvent方法)之前调用。

    event.initUIEvent(type, canBubble, cancelable, view, detail)

    该方法的参数含义,可以参见MouseEvent构造函数的部分。其中,detail参数是一个数值,含义与事件类型有关,对于鼠标事件,这个值表示鼠标按键在某个位置按下的次数。

    var e = document.createEvent("UIEvent");
    e.initUIEvent("click", true, true, window, 1);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值