欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

《JavaScript高级程序设计》笔记:事件(十三)

程序员文章站 2022-09-04 15:14:05
事件流 事件冒泡 IE的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(文档)。如下代码: 如果你点击了上面的div元素,那么这个click事件会按照如下顺序传播: (1)
(2) (3) (4)document 所有现代浏览 ......

事件流

事件冒泡

ie的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(文档)。如下代码:

<body>
    <div id="mydiv">click me</div>
</body>

如果你点击了上面的div元素,那么这个click事件会按照如下顺序传播:

  • (1)<div>
  • (2)<body>
  • (3)<html>
  • (4)document

所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。ie5.5以及更早的版本事件冒泡会跳过<html>元素(从<body>直接跳到document)。ie9及其它浏览器则将事件一直冒泡到window对象。

事件捕获

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。如上面的例子,单机div元素,那么就会按照以下顺序触发click事件:

  • (1)document
  • (2)<html>
  • (3)<body>
  • (4)<div>

由于老版本浏览器的不支持,因此很少有人使用事件捕获。

dom事件流

“dom2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

事件处理程序

html事件处理程序

<input type="button" value="click me" onclick="alert(this.value)"/>

dom0级事件处理程序

var btn = document.getelementbyid('mybtn');
btn.onclick = function(){
    console.log(this.value);
}

dom2级事件处理程序

“dom2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addeventlistener()removeeventlistener()。接受三个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。布尔值为true,表示在捕获阶段调用事件处理程序;如果为false,表示在冒泡阶段调用事件处理程序。

var btn = document.getelementbyid('mybtn');
btn.addeventlistener('click',function(){
    alert(this.value)
},false)

ie事件处理程序

ie中实现了与dom中类似的两个方法:attachevent()detachevent()。这两个方法接受相同的两个参数:事件处理程序名称和事件处理程序函数。由于ie8及更早版本只支持事件冒泡,所以通过attachevent()添加的事件处理程序都会被添加到冒泡阶段。

btn.attachevent('onclick',function(){
    alert(this.value)  //undefined
})

结果为undefined的原因是,在使用attachevent()方法的情况下,事件处理程序会在全局作用域中运行。因此this等于window。在编写跨浏览器区别的时候,牢记这一区别非常重要。

attachevent()方法也可以为一个元素添加多个事件处理程序,如下:

btn.attachevent('onclick',function(){
    alert('clicked')
})
btn.attachevent('onclick',function(){
    alert('hello world!')
})

在ie9以及更改版本浏览器得到的结果顺序跟addeventlistener()的顺序一样,结果是先clicked,后hello world!。在ie8以及以前版本得到顺序是相反的。

跨浏览器的事件处理程序

var eventutil = {
    addhandler:function(element,type,handler){
        if(element.addeventlistener){
            element.addeventlistener(type,handler,false);
        }else if(element.attachevent){
            element.attachevent("on" + type,handler);
        }else{
            element["on" + type] = handler;
        }
    },
    removehandler:function(element,type,handler){
        if(element.removeeventlistener){
            element.removeeventlistener(type,handler,false);
        }else if(element.detachevent){
            element.detachevent("on" + type,handler);
        }else{
            element["on" + type] = null;
        }
    }
}

调用方式:

var handler = function(){
    alert('clicked');
}
eventutil.addhandler(btn,"click",handler);

//这里省略其它代码
eventutil.removehandler(btn,"click",handler);

事件对象

 dom中的事件对象

兼容dom的浏览器会将一个event对象传入到事件处理程序中,无论指定事件处理程序时用什么方法(dom0级或dom2级),都会传入event对象。下面的例子:

var btn = document.getelementbyid('mybtn');

btn.onclick = function(event){
    console.log(event.type); //click
}
btn.addeventlistener('click',function(event){
    console.log(event.type); //click
},false)

在通过html特性指定事件处理程序时,变量event中保存着event对象,下面例子:

<input type="button" value="click me" onclick="alert(event.type)"/>

event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员:

  •  bubbles:(boolean)只读,表明事件是否冒泡。
  • cancelable:(boolean)只读,表明是否可以取消事件的默认行为。
  • currenttarget:(element)只读,其事件处理程序当前正在处理事件的那个元素。
  • defaultprevented:(boolean)只读,为true表明已经调用了preventdefault()(dom3级事件中新增)。
  • detail:(integer)只读,与事件相关的细节信息。
  • eventphase:(integer)只读,调用事件处理程序的阶段:1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段。
  • preventdafult():(function)只读,取消事件的默认行为。如果cancelable是true,则可以使用这个方法。
  • stopimmediatepropagation():(function)只读,取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(dom3级事件中新增)。
  • stoppropagation():(function)只读,取消事件的进一步捕获或冒泡。如果过bubbles为true,则可以使用这个方法。
  • target:(element)只读,事件的目标。
  • trusted:(boolean)只读,为true表示事件是浏览器生成的,为false表示事件是有开发人员通过javascript创建的(dom3级事件中新增)。
  • type:(string)只读,被触发的事件的类型。
  • view:(abstractview)只读,与事件关联的抽象视图。等同于发生事件的window对象。

在事件处理程序内部,对象this始终等于currenttarget的值,而target只包含事件的实际目标。如果直接将事件处理程序指向给了目标元素,则this,currenttarget和target包含相同的值,如下代码:

var btn = document.getelementbyid('mybtn');

btn.addeventlistener('click',function(event){
    console.log(event.currenttarget === this); //true
    console.log(event.target === this); //true
},false)

如果事件处理程序存在于按钮的父节点中(例如document.body),那么这些值是不相等的,如下代码:

var btn = document.getelementbyid('mybtn');

document.body.addeventlistener('click',function(event){
    console.log(event.currenttarget === document.body); //true
    console.log(document.body === this); //true
    console.log(event.target === btn); //true
},false)

 在需要一个函数处理多个事件时,可以使用type属性,如下:

var btn = document.getelementbyid('mybtn');
var handler = function(event){
    switch(event.type){
        case "click":
            console.log("clicked");
            break;
        case "mouseover":
            event.target.style.backgroundcolor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundcolor ="";
            break;
    }
};

btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

阻止事件的默认行为,使用preventdefault()方法,比如阻止a标签的跳转,如下代码:

var link = document.getelementbyid('mylink');
link.onclick = function(event){
    event.preventdefault();
}

只有cancelable属性设置为true时,才可以使用preventdefault()来取消其默认行为。

stoppropagation()方法用于立即停止事件在dom层次中的传播,寄取消进一步的事件捕获或冒泡。例如直接添加到一个按钮的事件处理程序可以调用stoppropagation(),从而避免触发注册在document.body上面的事件处理程序,如下代码:

var btn = document.getelementbyid('mybtn');
btn.onclick = function(event){
    console.log('clicked');
    event.stoppropagation();
}
document.body.onclick = function(){
    console.log('body clicked');
}

结果只打印出了clicked。

事件对象的eventphase属性,可以用来确定事件当前正处理事件流的哪个阶段。1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段。

var btn = document.getelementbyid('mybtn');
btn.onclick = function(event){
    console.log(event.eventphase);//2
};

document.body.addeventlistener('click',function(event){
    console.log(event.eventphase);//1
},true);

document.body.onclick = function(){
    console.log(event.eventphase);//3
};

显示结果顺序分别为1,2,3。

 ie中的事件对象

与访问dom中的event对象不同,要访问ie中的event对象有几种不同的方式,取决于事件处理程序的方法。在使用dom0级方法添加事件处理程序时,event对象作为window对象的一个属性存在,如下例子:

var btn = document.getelementbyid('mybtn');
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //click
};

如果事件程序是使用attachevent()添加的,那么就会有一个event对象作为参数被传递到事件处理程序函数中,如下代码:

var btn = document.getelementbyid('mybtn');
btn.attachevent('onclick',function(event){
    alert(event.type); //click
})

 ie中的event对象包含下面的属性和方法:

  • cancelbubble:(boolean)读/写,默认值为false,将其设置为true就可以取消事件冒泡(与dom中的stoppropagation()方法的作用相同)。
  • returnvalue:(boolean)读/写,默认值为true,将其设置为false可以取消事件的默认行为(与dom中的preventdafult()方法的作用相同)。
  • srcelement:(element)只读,事件的目标(与dom中的target属性相同)。
  • type:(string)只读,被触发的事件的类型。

 因为事件处理程序中的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于事件目标。故而,最好还是使用event.srcelement比较保险,如下代码:

var btn = document.getelementbyid('mybtn');
btn.onclick = function(){
    alert(window.event.srcelement === this); //true
};

btn.attachevent('onclick',function(event){
    alert(this); //window
    alert(window.event.srcelement === this); //false
})

取消默认行为:

var link = document.getelementbyid('mylink');
link.onclick = function(){
    window.event.returnvalue = false;
};

停止事件冒泡:

var btn = document.getelementbyid('mybtn');
btn.onclick = function(){
    alert('clicked');
    window.event.cancelbubble = true;
};

document.body.attachevent('onclick',function(event){
    alert('body clicked')
})

 跨浏览器的事件对象

var eventutil = {
    
    //事件处理程序
    addhandler:function(element,type,handler){
        if(element.addeventlistener){
            element.addeventlistener(type,handler,false);
        }else if(element.attachevent){
            element.attachevent("on" + type,handler);
        }else{
            element["on" + type] = handler;
        }
    },
    
    //得到event对象
    getevent:function(event){
        return event ? event : window.event;
    },
    
    //得到事件目标
    gettarget:function(event){
        return event.target || event.srcelement;
    },
    
    //取消默认行为
    preventdefault:function(event){
        if(event.preventdefault){
            event.preventdefault();
        }else{
            event.returnvalue = false;
        }
    },
    
    //移除事件处理程序
    removehandler:function(element,type,handler){
        if(element.removeeventlistener){
            element.removeeventlistener(type,handler,false);
        }else if(element.detachevent){
            element.detachevent("on" + type,handler);
        }else{
            element["on" + type] = null;
        }
    },
    
    //阻止事件捕获或冒泡
    stoppropagation:function(event){
        if(event.stoppropagation){
            event.stoppropagation();
        }else{
            event.cancelbubble = true;
        }
    }
}

 事件类型

“dom3级事件”规定如下几类事件。

  • ui(user interface,用户界面)事件,当用户与页面上的元素交互时触发;
  • 焦点事件,当元素获得或者失去焦点时触发;
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发;
  • 滚轮事件,当鼠标使用滚轮(或类似设备)时触发;
  • 文本事件,当在文档中输入文本时触发;
  • 键盘事件,当用户通过键盘在页面上执行操作时触发;
  • 合成事件,当为ime(input method editor,输入法编辑器)输入字符时触发;
  • 变动(mutation)事件,当底层dom结构发生变化时触发;
  • 变动名称事件,当元素或属性名变动时触发。此类事件已经废除。

ui事件

  • domactivate:表示元素已经被用户操作(通过鼠标或键盘)激活。这个事件在dom3级事件中被废除,但firefox2+和chrome支持它。
  • load:当页面完全加载后在window上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>元素上面触发,或者当嵌入的内容加载完毕时在<object>元素上面触发。
  • unload:当页面完全卸载后在window上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后早<object>元素上面触发。
  • abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发。
  • error:当发送javascript错误时在window上面触发,当无法加载图片是在<img>元素上面触发,当无法加载嵌入内容时在<object>元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。
  • select:当用户选择文本框(<input>或<textarea>)中的一或多个字符时触发。
  • resize:当窗口或框架的大小发生变化时在window或框架上面触发。
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上触发。<body>元素中包含所加载页面的滚动条。

多数这些事件都与window对象或者表单控件相关。

除了domactivate之外,其它事件在dom2级事件中都归为html事件(domactivate在dom2级事件中仍属于ui事件)。要确定浏览器是否支持dom2级事件规定的html事件,可以用如下代码:

var issupported = document.implementation.hasfeature("htmlevents","2.0");

确定浏览器是否支持dom3级事件定义的事件,代码如下:

var issupported = document.implementation.hasfeature("uievent","3.0");

1.load事件

eventutil.addhandler(window,'load',function(event){
    console.log('loaded!');
})

为<body>元素添加一个onload特性,代码如下:

<body onload="alert('loaded!')">
</body>

一般在window上面发生的任何事件都可以在<body/>元素中通过相应的特性来指定,因为在html中无法访问到window元素。建议尽可能使用javascript方式。

图片加载:

var image = document.getelementbyid('myimage');
eventutil.addhandler(image,'load',function(event){
    event = eventutil.getevent(event);
    console.log(eventutil.gettarget(event).src);
})

待创建新的<img>元素时,可以为其指定一个事件处理程序。此时,最重要的是在指定src属性之前先指定事件,如下代码:

eventutil.addhandler(window,'load',function(){
    var image = document.createelement('img');
    eventutil.addhandler(image,'load',function(event){
        event = eventutil.getevent(event);
        console.log(eventutil.gettarget(event).src);
    })
    document.body.appendchild(image);
    image.src = 'images/b.jpg';
})

需要格外注意的一点是:新图像元素不一定要从添加到文档后才开始下载,只要设置了src属性就开始下载

同样的功能可以使用dom0级的image对象实现,如下代码:

eventutil.addhandler(window,'load',function(){
    var image = new image();
    eventutil.addhandler(image,'load',function(event){
        event = eventutil.getevent(event);
        console.log(eventutil.gettarget(event).src);
    })
    image.src = 'images/b.jpg';
})

还有一些元素以非标准的的方式支持load事件。在ie9以及更高版本,<script>元素也会触发load事件。

eventutil.addhandler(window,'load',function(){
    var script = document.createelement('script');
    eventutil.addhandler(script,'load',function(event){
        console.log('loaded!');
    })
    script.src = 'js/common.js';
    document.body.appendchild(script);
})

注:ie8以及更早版本不支持<script>元素上的load事件。

<link>元素的load事件:

eventutil.addhandler(window,'load',function(){
    var link = document.createelement('link');
    link.type ="text/css";
    link.rel ="stylesheet";
    eventutil.addhandler(link,'load',function(event){
        console.log('loaded!');
    })
    link.href = 'css/rest.css';
    document.getelementsbytagname('head')[0].appendchild(link)
})

与<script>节点类似,在未指定href属性并将<link>元素添加到文档之前也不会开始下载样式表。

2.unload事件

这个事件在文档完全被卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。而利用这个事件最多的情况就是清除引用,以避免内存泄露。

eventutil.addhandler(window,'unload',function(){
    alert('unloaded!');
})

3.resize事件

eventutil.addhandler(window,'resize',function(){
    alert('resized!');
})

4.scroll事件

eventutil.addhandler(window,'scroll',function(){
    if(document.compatmode == 'css1compat'){
        console.log(document.documentelement.scrolltop);
    }else{
        console.log(document.body.scrolltop);
    }
})

焦点事件

焦点事件会在页面获得或者失去焦点时触发。利用这些事件并与document.hasfocus()方法以及document.activeelement属性配合,可以知晓用户在页面中的行踪,以下6个焦点事件。

  • blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
  • domfocusin:在元素获得焦点时触发。这个事件与html事件focus等价,但它冒泡。只有opera支持这个事件。dom3级事件废除了domfocusin,选择了focusin。
  • domfocusout:在元素失去焦点时触发。这个事件是html事件blur的通用版本。只有opera支持这个事件。dom3级事件废除了domfocusout,选择了focusout。
  • focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
  • focusin:在元素获得焦点时触发。这个事件与html事件focus等价,但它冒泡。支持这个事件的浏览器有ie5.5+、safari5.1+、opera11.5+和chrome。
  • focusout:在元素失去焦点时触发。这个事件是html事件blur的通用版本。支持这个事件的浏览器有ie5.5+、safari5.1+、opera11.5+和chrome。

ie的focusin和focusout最后被dom3级事件采纳为标准方式。

当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:

(1)focusout在失去焦点的元素上触发。

(2)focusin在获得焦点的元素上触发。

(3)blur在失去焦点的元素上触发。

(4)domfocusout在失去焦点的元素上触发。

(5)focus在获得焦点的元素上触发。

(6)domfocusin在获得焦点的元素上触发。

确定浏览器是否支持这些事件,可以使用如下代码:

var issupported = document.implementation.hasfeature('focusevent','3.0');

鼠标与滚轮事件

dom3级事件中定义了9个鼠标事件,如下:

  • click:在用户单机主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。
  • dblclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。dom3级纳入了标准。
  • mousedown:在用户按下任意鼠标按钮时触发。不能通过键盘触发这个事件。
  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。dom3级事件将它纳入了规范。
  • mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。dom3级事件将它纳入了规范。
  • mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。
  • mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
  • mouseover:当鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。
  • mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。

页面上的所有元素都支持鼠标事件。除了mouseentermouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件和其他事件是密不可分的关系。

只有在同一个元素上相继触发mousedown和mouseup事件,才会触发click事件;如果mousedown或mouseup中的一个被取消,就不会触发click事件。类似地,只有触发两次click事件,才会触发一次dblclick事件,如果有代码阻止了连续两次触发click事件(可能是直接取消click事件,也可能通过取消mousedown或mouseup间接实现),那么就不会触发dblclick事件。这4个事件触发的顺序如下:

(1)mousedown

(2)mouseup

(3)click

(4)mousedown

(5)mouseup

(6)click

(7)dblclick

ie8及之前版本的实现有一个小bug,因此在双击事件中,会跳过第二个mousedown和click事件,其顺序如下:

(1)mousedown

(2)mouseup

(3)click

(4)mouseup

(5)dblclick

ie9修复了这个bug。

使用如下代码可以检测浏览器是否支持如上dom2级事件(除dblckick、mouseenter和mouseleave之外):

var issupported = document.implementation.hasfeature('mouseevents','2.0');

检测浏览器是否支持上面的所有事件,代码如下:

var issupported = document.implementation.hasfeature('mouseevent','3.0');

1.客户区坐标位置

clientxclienty他们的值表示事件发生时鼠标指针在视口中的水平和垂直坐标。

var btn = document.getelementbyid('mybtn');
eventutil.addhandler(btn,'click',function(event){
    event = eventutil.getevent(event);
    console.log("client coordinates:" + event.clientx + "," + event.clienty);
})

2.页面坐标位置

pagexpagey,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。

以下代码可以取得鼠标事件在页面中的坐标:

var btn = document.getelementbyid('mybtn');
eventutil.addhandler(btn,'click',function(event){
    event = eventutil.getevent(event);
    console.log("page coordinates:" + event.pagex + "," + event.pagey);
})

在页面没有滚动的情况下,pagex和pagey的值与clientx和clienty的值相等。

ie8及更早的版本不支持事件对象上的页面坐标,不过可以使用客户区坐标和滚动信息可以计算出来。这时候需要用到document.body(混杂模式)或document.documentelement(标准模式)中的scrollleft和scrolltop属性。计算代码如下:

var btn = document.getelementbyid('mybtn');
eventutil.addhandler(btn,'click',function(event){
    event = eventutil.getevent(event);
    var pagex = event.pagex,
        pagey = event.pagey;
    if(pagex === undefined){
        pagex = event.clientx + (document.body.scrollleft || document.documentelement.scrollleft);
    }
    if(pagey === undefined){
        pagey = event.clienty + (document.body.scrolltop || document.documentelement.scrolltop);
    }
    console.log("page coordinates:" + pagex + "," + pagey);
})

3.屏幕坐标位置

screenxscreeny属性表示鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。

屏幕坐标:

var btn = document.getelementbyid('mybtn');
eventutil.addhandler(btn,'click',function(event){
    event = eventutil.getevent(event);
    console.log("page coordinates:" + event.screenx + "," + event.screeny);
})

4.修改键

虽然鼠标事件主要是由鼠标来触发的,但在按下鼠标时键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键就是shiftctrlaltmeta(在windows键盘中的windows键,在苹果机中是cmd键),它们经常被用来修改鼠标事件的行为。dom为此规定了4个属性,表示这些修改键的状态:shiftkeyctrlkeyaltkeymetakey。这些属性中包含的都是布尔值,如果相应的键被按下了,则值为true,否则值为false。

当某个鼠标事件发生时,通过检测这几个属性可以确定是否用户同时按下了其中的键。如下例子:

var btn = document.getelementbyid('mybtn');
eventutil.addhandler(btn,'click',function(event){
    event = eventutil.getevent(event);
    var keys = new array();
    if(event.shiftkey){
        keys.push('shift');
    }
    if(event.ctrlkey){
        keys.push('ctrl');
    }
    if(event.altkey){
        keys.push('alt');
    }
    if(event.metakey){
        keys.push('meta');
    }
    console.log("keys:" + keys.join(','));
})

注:ie8以及之前的版本不支持metakey属性。

5.相关元素

在发生mouseovermouseout事件时,还会涉及更多的元素。这两个事件都会涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。对mouseover而言,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素。类似地,对于mouseout事件而言,事件的主目标就是失去光标的元素,而相关元素是获得光标的元素。来看下面一个例子:

<body>
    <div id="mydiv" style="background-color:red;width:100px;height:100px;"></div>
</body>

这个例子会在页面上显示一个<div>元素。如果鼠标指针一开始就在这个<div>元素上,然后移出了这个元素,那么就会在<div>元素上触发mouseout事件,相关元素就是<body>元素。与此同时,<body>元素上面会触发mouseenter事件,相关元素就变成了<div>。

dom通过event对象的relatedtarget属性提供了相关元素的信息。这个属性只对于mouseover和mouseout事件才包含值;对于其它事件,这个属性的值为null。ie8以及之前的版本不支持relatedtarget属性,但提供了保存着同样信息的不同属性。在mouseover事件触发时,ie中的fromelement属性中保存了相关元素;在mouseout事件触发时,ie的toelement属性保存着相关元素。(ie9支持这些所有属性。)把这个添加到eventutil对象中,如下:

var eventutil = {
    //省略了其它代码
    
    //得到相关元素信息
    getrelatedtarget:function(event){
        if(event.relatedtarget){
            return event.relatedtarget;
        }
        else if(event.toelement){
            return event.toelement;
        }
        else if(event.fromelement){
            return event.fromelement;
        }else{
            return null;
        }
    },
    
    //省略了其它代码
      
};

调用:

var mydiv = document.getelementbyid('mydiv');
eventutil.addhandler(mydiv,'mouseout',function(event){
    event = eventutil.getevent(event);
    var target = eventutil.gettarget(event);
    var relatetarget = eventutil.getrelatedtarget(event);
    console.log("moused out of" + target.tagname + "to" + relatetarget.tagname);  //moused out ofdivtobody
})

6.鼠标按钮

只有在主鼠标按钮被单击(或键盘回车键被按下)时才会触发click事件,因此检测按钮的信息不是必要的。但对于mousedownmouseup事件来说,则在其event对象存在一个button属性,表示按下或者释放的按钮。dom的button属性可能有如下3个值:

  • 0:表示主鼠标按钮;
  • 1:表示中间的鼠标按钮(鼠标滚轮按钮);
  • 2:表示次鼠标按钮;

在常规的设置中,主鼠标按钮就是鼠标左键,而次鼠标按钮就是鼠标右键。

ie8及之前的版本也提供了button属性,但这个属性的值与dom中的button属性有很大的差异。

  • 0:没有按下按钮;
  • 1:表示按下了主鼠标按钮;
  • 2:表示按下了次鼠标按钮;
  • 3:表示同时按下了主次鼠标按钮;
  • 4:表示按下了中间的鼠标按钮;
  • 5:表示同时按下了主鼠标按钮和中间的鼠标按钮;
  • 6:表示同时按下了次鼠标按钮和中间的鼠标按钮;
  • 7:表示同时按下了三个鼠标按钮;

由于单独使用能力检测无法确定差异(两种模型有同名的button属性),因此必须另辟蹊径。我们知道,支持dom版鼠标事件的浏览器课可以通过hasfearture()方法检测,所有可以再为eventutil对象添加getbutton()方法:

//得到button属性
    getbutton:function(event){
        if(document.implementation.hasfeature("mouseevents","2.0")){
            return event.button;
        }else{
            switch(event.button){
                case 0:
                case 1:
                case 3:
                case 5:
                case 7:
                    return 0;
                case 2:
                case 6:
                    return 2;
                case 4:
                    return 1;
            }
        }
    },

调用:

var mydiv = document.getelementbyid('mydiv');
eventutil.addhandler(mydiv,'mousedown',function(event){
    event = eventutil.getevent(event);
    alert(eventutil.getbutton(event));
})

7.更多的事件信息

“dom2级事件”规范在event对象中还提供了detail属性,用于给出有关事件的更多信息。对于鼠标事件来说,detail中包含了一个数值,表示在给定位置上发生了多少次单击。在同一个像素上相继的发生一次mousedown和一次mouseup事件算作一次单击。detail属性从1开始计数,每次单击发生后都会递增。如果鼠标在mousedown和mouseup之间移动了位置,则detail会被重置为0。

ie也通过下列属性为鼠标事件提供了更多信息。

  • altleft:布尔值,表示是否按下了alt键。
  • ctrlleft:布尔值,表示是否按下了ctrl键。
  • offsetx:光标相对于目标元素边界的x坐标。
  • offsety:光标相对于目标元素边界的y坐标。
  • shiftleft:布尔值,表示是否按下了shift键。

这些属性用处不大,只有ie支持他们,另一方面他们提供的信息要么没有什么用,要么可以通过其他方式计算得来。

8.鼠标滚轮事件

ie6.0首先实现了mousewheel事件。这个事件可以在任何元素上面触发,最终会冒泡到document(ie8)或window(ie9、opera、chrome及safari)对象。与mousewheel事件对应的event对象除了包含鼠标事件的所有标准信息外,还包含一个特殊的wheeldelta属性。当用户向前滚动鼠标滑轮时,wheeldelta是120的倍数;当用户向后滚动鼠标滑轮时,wheeldelta是-120的倍数。

eventutil.addhandler(document,'mousewheel',function(event){
    event = eventutil.getevent(event);
    console.log(event.wheeldelta);
})

多数情况下,只需要知道滚轮的滚动方向就够了,而这通过检测wheeldelta的正负号就可以确定。

注意的是,在opera9.5之前的版本中,wheeldelta值的正负号是颠倒的。如果需要支持早期的opera版本,代码如下:

eventutil.addhandler(document,'mousewheel',function(event){
    event = eventutil.getevent(event);
    var delta = (client.engine.opera && client.engine.opera < 9.5 ? -event.wheeldelta : event.wheeldelta);
    console.log(delta);
})

firefox支持一个名为dommousescroll的类似事件,也是在鼠标滚轮滚动时触发。鼠标滚轮滚动信息保存在detail属性中,当向前滚动鼠标滚轮时,这个属性的值为-3的整数倍,当向后滚动鼠标滚轮时,这个属性的值是3的整数倍。

eventutil.addhandler(document,'dommousescroll',function(event){
    event = eventutil.getevent(event);
    console.log(event.detail);
})

跨浏览器总结,添加到eventutil对象中:

//取得鼠标滚轮增量值(delta)
    getwheeldelta:function(event){
        if(event.wheeldelta){
            return event.wheeldelta;
        }else{
            return -event.detail * 40;
        }
    },

调用方式:

(function(){
    function handlemousewheel(event){
        event = eventutil.getevent(event);
        var delta = eventutil.getwheeldelta(event);
        console.log(delta);
    };
    
    eventutil.addhandler(document,"mousewheel",handlemousewheel);
    eventutil.addhandler(window,"dommousescroll",handlemousewheel);
})();

9.触摸设备

 ios和android的实现非常特别,因为这些设备没有鼠标。在面向iphone和ipad中的safari开发时,要记住以下几点:

  • 不支持dblclick事件。双击浏览器窗口会放大页面,而且没有办法改变该行为。
  • 轻击可单击元素会触发mousemove事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生mousedown、mouseup和click事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了onclick事件处理程序的元素。
  • mousemove事件也会触发mouseover和mouseout事件。
  • 两个手指放在屏幕上且页面随手指移动而滚动时会触发mousewheel和scroll事件。

10.无障碍性问题

如果你的web应用程序或者网站要确保残疾人特别是那些使用屏幕阅读器的人都能访问,那么在使用鼠标事件时就要格外小心。前面提到过,可以通过键盘上的回车键来触发click事件,但其他鼠标事件却无法通过键盘来触发。为此,我们不建议使用click之外的其他鼠标事件来展示功能或者引发代码执行。因为这样会给盲人或视障用户造成极大不便。

键盘与文本事件

“dom3级事件”为键盘事件制定了规范。有3个键盘事件如下:

  • keydown:当用户按下键盘上的任意键时触发,而且按住不放的话,会重复触发该事件。
  • keypress:当用户按下键盘上的字符键时触发,而且按住不放的话,会重复触发该事件。按下esc键也会触发该事件。safari3.1之前的版本也会在用户按下非字符键时触发该事件。
  • keyup:当用户释放键盘上的键时触发。

只有一个文本事件:textinput。这个事件是对keypress的补充,用意是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发textinput事件。

在用户按下键盘上的字符键时,键盘执行顺序:keydown、keypress、keyup。其中keydown、keypress都是在文本框发生变化之前被触发的;而keyup是在文本框已经发生变化后触发。如果用户按下一个字符键不放,那么会重复触发keydown与keypress,直到用户松开该键为止。

如果用户按下的是一个非字符键时,执行顺序:keydown、keyup。

1.键码

在发生keydown与keyup事件时,event对象的keycode属性包含一个代码,与键盘上一个特定的键对应。dom和ie中的event对象都支持keycode属性。如下例子:

var textbox = document.getelementbyid('mytext');
eventutil.addhandler(textbox,'keyup',function(event){
    event = eventutil.getevent(event);
    console.log(event.keycode);
})

常用非字符键的键码:

左箭头:37;上箭头:38;右箭头:39;下箭头40;上翻页:33;下翻页:34;退出(esc):27。

无论keydown或者keyup事件都会存在一些特殊情况。在firefox和opera中,按分号键时keycode为59,也就是ascii中分号的编码;但在ie,safari,chrome中返回186,即键盘中按键的键码。

2.字符编码

ie9、firefox、chrome和safari的event对象都支持一个charcode属性,这个属性只有在发生keypress事件时才包含值,而且这个值是按下的那个键所代码字符的ascii编码。ie8及之前的版本和opera则是在keycode中保存字符的ascii编码。下面以跨浏览器取得字符编码,放在eventutil对象中:

getcharcode:function(event){
        if(typeof event.charcode == 'number'){
            return event.charcode;
        }else{
            return event.keycode;
        }
    },

使用方式:

var textbox = document.getelementbyid('mytext');
eventutil.addhandler(textbox,'keypress',function(event){
    event = eventutil.getevent(event);
    console.log(eventutil.getcharcode(event));
})

在取得字符编码后,就可以使用string.fromcharcode()将其转化为实际的字符。如下:

var textbox = document.getelementbyid('mytext');
eventutil.addhandler(textbox,'keypress',function(event){
    event = eventutil.getevent(event);
    var charcode = eventutil.getcharcode(event);
    var text = string.fromcharcode(charcode);
})

3.dom3级变化

dom3级事件中的键盘事件,不再包含charcode属性,而是包含两个新属性:keychar,由于这个两个属性各浏览器支持程度不一样,不推荐使用。

dom3级事件在添加一个location属性,也不推荐使用。

4.textinput事件

“dom3级事件”规范中引入了一个新事件,textinput。根据规范,当用户在可编辑区域中输入字符时,就会触发这个事件。keypress和textinput的区别:

  • 任何可以获得焦点的元素都可以触发keypress事件,但只有可编辑区域才能触发textinput事件。
  • textinput事件只有在用户按下能够输入实际字符的键时才会触发,而keypress事件则在按下那些能够影响文本显示的键也会触发(例如退格键)。

由于textinput事件主要考虑的是字符,因此它的event对象中还包含一个data属性。这个属性的值就是用户输入的字符(而非字符编码)。

var textbox = document.getelementbyid('mytext');
eventutil.addhandler(textbox,'textinput',function(event){
    event = eventutil.getevent(event);
    console.log(event.data)
})

html5事件

1.contextmenu事件

eventutil.addhandler(window,'load',function(event){
    var div = document.getelementbyid('mydiv');
     eventutil.addhandler(div,'contextmenu',function(event){
         event = eventutil.getevent(event);
         eventutil.preventdefault(event);
         
         var menu = document.getelementbyid('mymenu');
         menu.style.left = event.clientx + 'px';
         menu.style.top = event.clienty + 'px';
         menu.style.visibility = 'visible';
     })
     
     eventutil.addhandler(document,'click',function(event){
         document.getelementbyid('mymenu').style.visibility = 'hidden';
     })
})

2.beforeunload事件

之所以有发生在window对象上的beforeunload事件,是为了让开发人员在页面卸载前阻止这一操作。可以通过这个事件来取消卸载并继续使用原有页面。

eventutil.addhandler(window,'beforeunload',function(event){
    event = eventutil.getevent(event);
    var message = "你确定要离开这个页面吗?";
    event.returnvalue = message;
    return message;
})

3.domcontentloaded事件

window的load事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而颇费周折。而domcontentloaded事件则在形成完整的dom树之后就会触发,不理会图像、js文件、css文件或其它资源是否已经下载完毕。与load事件不同,domcontentloaded支持在页面下载的早期添加事件处理程序,这也就意味着用户能够尽早地与页面进行交互。

要处理domcontentloaded事件,可以为document或window添加相应的事件处理程序(尽管这个事件会冒泡到window,但它的目标实际上是document)。

eventutil.addhandler(document,"domcontentloaded",function(event){
    alert('content loaded');
})

ie9以及其它浏览器支持该事件,对于不支持domcontentloaded的浏览器,建议在页面加载期间设置一个时间为0毫秒的超时调用,如下:

settimeout(function(){
    //在此添加事件处理程序
},0)

4.pageshow和pagehide事件

往返缓存(back-forward cache,或bfcache),用户使用浏览器的“后退”、“前进”按钮加快页面的转换速度。将整个页面保存在内存里。

(function(){
    var showcount = 0;
    eventutil.addhandler(window,'load',function(){
        alert('loaded fired')
    });
    
    eventutil.addhandler(window,"pageshow",function(event){
        showcount++;
        alert("show has been fired" + showcount + "times.");
    });
})()

设备事件

1.orientationchange事件

2.mozorientation事件

3.deviceorientation事件

4.devicemotion事件

触摸与手势事件

触摸事件如下:

  • touchstart:当手指触摸屏幕时触发。
  • touchmove:当手指在屏幕滑动时连续的触发。在这个事件发生期间,可以调用preventdefault()可以阻止滚动。
  • touchend:当手指从屏幕上移开时触发。
  • touchcancel:当系统停止跟踪触摸时触发。

上面这几个事件都会冒泡,也都可以取消。它们是以兼容dom的方式实现的。因此,每个触摸事件的event对象都提供了在鼠标事件中常见的属性:bubbles 、cancelabel 、view 、clientx 、clienty 、screenx、 screeny 、detail 、altkey、 ctrlkey、 shiftkey 和metakey。

除了常见的dom属性外,触摸事件还包括下列三个用于跟踪触摸的属性。

  • touches:表示当前跟踪的触摸操作的touch对象的数组。
  • targettouchs:特定于事件目标的touch对象的数组。
  • changedtouches:表示自上次触摸以来发生了什么改变的touch对象的数组。

每个touch对象包含下列属性:

  • clientx:触摸目标在视口中的x坐标。
  • clienty:触摸目标在视口中的y坐标。
  • identifier:标识触摸的唯一id。
  • pagex:触摸目标在页面中的x坐标。
  • pagey:触摸目标在页面中的y坐标。
  • screenx:触摸目标在屏幕中的x坐标。
  • screeny:触摸目标在屏幕中的y坐标。
  • target:触摸的dom节点目标。

如下面例子:

 

function handlertouchevent(event){
    
    //只跟踪一次触摸
    if(event.touches.length == 1){
        var output = document.getelementbyid('output');
        switch(event.type){
            case "touchstart":
                output.innerhtml = "touch started(" + event.touches[0].clientx + "," + event.touches[0].clienty + ")";
                break;
            case "touchend":
                output.innerhtml += "<br/>touch ended(" + event.changedtouches[0].clientx + "," + event.changedtouches[0].clienty + ")";
                break;
            case "touchmove":
                event.preventdefault(); //阻止滚动
                output.innerhtml += "<br/>touch moved(" + event.changedtouches[0].clientx + "," + event.changedtouches[0].clienty + ")";
                break;
        }
    }
}

eventutil.addhandler(document,"touchstart",handlertouchevent);
eventutil.addhandler(document,"touchend",handlertouchevent);
eventutil.addhandler(document,"touchmove",handlertouchevent);

 注意,在touchend事件发生时,touches集合中就没有任何的touch对象了,因为不存在活动的触摸操作;此时就必须转而使用changedtouches集合。

在触摸屏幕上的元素时,这些事件(包括鼠标事件)发生的顺序如下:

  • 1.touchstart
  • 2.mouseover
  • 3.mousemove(一次)
  • 4.mousedown
  • 5.mouseup
  • 6.click
  • 7.touched

目前只有ios版的safari支持多点触摸。

内存和性能

在js中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的dom访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的角度出发,还是有一些办法能够提升性能的。

事件委托

事件委托利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会冒泡到document层次。也就是说,我们可以为整个页面添加一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

如下代码,点击3个li分别执行不同的操作:

 html代码:

<ul id="mylinks">
    <li id="gosomewhere">go somewhere</li>
    <li id="dosomething">do something</li>
    <li id="sayhi">say hi</li>
</ul>

js代码:

var list = document.getelementbyid('mylinks');
eventutil.addhandler(list,'click',function(event){
    event = eventutil.getevent(event);
    var target = eventutil.gettarget(event);
    switch(target.id){
        case "gosomewhere":
            document.title = "i changed the document's title";
            break;
        case "dosomething":
            window.location.href = "https://www.baidu.com";
            break;
        case "sayhi":
            alert('hi');
            break;
    }
})

 移除事件处理程序

html:

<div id="mydiv">
    <input type="button" value="click me" id="mybtn"/>
</div>

js:

var btn = document.getelementbyid('mybtn');
btn.onclick = function(){
    //先执行某些操作
    btn.onclick = null;
    document.getelementbyid('mydiv').innerhtml = 'processing...';
}

我们在设置<div>的innerhtml属性之前,先移除了按钮的事件处理程序。这样就确保了内存可以被再次利用。

注意,在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。

模拟事件

dom中的事件模拟

可以在document对象上使用createevent()方法创建event对象。这个方法接收一个参数,表示要创建的事件类型的字符串。在dom2级中,所有这些字符串都使用英文复数形式,而在dom3级中都变成了单数。这个字符串可以是下列字符串之一:

  • uievents:一般化的ui事件。鼠标事件和键盘事件都继承自ui事件。dom3级是uievent。
  • mouseevents:一般化的鼠标事件。dom3级是mouseevent。
  • mutationevents:一般化的dom变动事件。dom3级中是mutationevent。
  • htmlevents:一般化的html事件。没有对应的dom3级事件(html事件被分散到了其他类别中)。

在创建了event对象后,还需要使用与事件有关的信息对其进行初始化。每种类型的event对象都有一个特殊的方法,为它传入适当的数据就可以初始化该event对象。不同类型的方法的名字也不相同,这个取决于createevent()中使用的参数。

事件模拟的最后一步就是触发事件,使用dispatchevent()方法。调用dispatchevent()方法需要传入一个参数,即表示要触发事件的event对象。

模拟鼠标事件

为createevent()方法传入“mouseevents”,返回的对象有一个名为initmouseevent()方法,用于指定与该鼠标事件有关的信息。这个方法接收15个参数,分别与鼠标事件中的每个典型的属性一一对应,这些参数如下:

  • type(字符串):表示要触发的事件类型,例如“click”。
  • bubbles(布尔值):表示事件是否应该冒泡。为精确的模拟鼠标事件,应该把这个参数设置为true。
  • cancelable(布尔值):表示事件是否可以取消。为精确的模拟鼠标事件,应该把这个参数设置为true。
  • view:与事件关联的视图。这个参数几乎总是要设置为document.defaultview。
  • detail(整数):与事件有关的详细信息。这个值一般只有事件处理程序使用,但通常都设置为0。
  • screenx(整数):事件相对于屏幕的x坐标。
  • screeny(整数):事件相对于屏幕的y坐标。
  • clientx(整数):事件相对于视口的x坐标。
  • clienty(整数):事件相对于视口的y坐标。
  • ctrlkey(布尔值):表示是否按下的ctrl键。默认值为false。
  • altkey(布尔值):表示是否按下的alt键。默认值为false。
  • shiftkey(布尔值):表示是否按下的shift键。默认值为false。
  • metakey(布尔值):表示是否按下的meta键。默认值为false。
  • button(整数):表示按下了哪一个鼠标键。默认值为0。
  • relatedtarget(对象):表示与事件相关的对象,这个参数只有在模拟mouseover或mouseout时使用。

默认对按钮的单击事件:

var btn = document.getelementbyid('mybtn');
//创建事件对象
var event = document.createevent('mouseevents');
//初始化对象
event.initmouseevent('click',true,true,document.defaultview,0,0,0,0,0,false,false,false,false,0,null);
//触发事件
btn.dispatchevent(event);

2.模拟键盘事件

dom3级规定,调用createevent()并传入“keyboardevent”就可以创建一个键盘事件。返回的事件对象包含一个initkeyboardevent()方法,这个方法接收如下参数:

  • type(字符串):表示要触发的事件类型,例如“click”。