1. Mécanisme de propagation des événements
Le programme JavaScript côté client (c'est-à-dire le navigateur) adopte un modèle de programmation asynchrone basé sur les événements. Les navigateurs Web génèrent des événements lorsque quelque chose d'intéressant arrive à un document, un navigateur, un élément ou un objet qui lui est associé. Si une application JavaScript s'intéresse à un type spécifique d'événement, elle peut enregistrer une ou plusieurs fonctions à appeler lorsque de tels événements se produisent. Bien entendu, ce style n’est pas propre à la programmation Web ; toutes les applications qui utilisent des interfaces utilisateur graphiques l’adoptent.
Puisque nous souhaitons expliquer le traitement des événements en détail, commençons par quelques concepts de base :
①Type d'événement : est une chaîne utilisée pour décrire le type d'événement qui s'est produit. Par exemple, « mousemove » signifie que l'utilisateur a déplacé la souris et « keydown » signifie qu'une touche du clavier a été enfoncée. Le type d'événement est simplement une chaîne, parfois appelée nom de l'événement
;②Cible de l'événement : est un objet où un événement se produit ou y est lié. Les objets Window, Document et Element sont les cibles d'événements les plus courantes. Bien entendu, l'objet XMLHttpRequest en AJAX est également une cible d'événement
;③Gestionnaire d'événements : est une fonction qui traite ou répond aux événements. Elle est également appelée écouteur d'événements. Les applications enregistrent leurs gestionnaires d'événements auprès des navigateurs Web en spécifiant le type et la cible de l'événement.
④Objet Événement : est un objet lié à un événement spécifique et contenant des informations détaillées sur l'événement. L'objet événement est transmis à la fonction de traitement des événements en tant que paramètre (mais dans IE8 et les versions précédentes, la variable globale event est l'objet événement). Les objets événement ont un attribut type qui spécifie le type d'événement et un attribut target qui spécifie la cible de l'événement (mais dans IE8 et les versions précédentes, srcElement est utilisé à la place de target). Bien entendu, différents types d'événements définiront également d'autres propriétés uniques pour leurs objets événement associés. Par exemple, un objet associé à un événement de souris contiendrait les coordonnées du pointeur de la souris, tandis qu'un objet associé à un événement de clavier contiendrait les détails de la touche enfoncée et de la touche de modification.
Ce qui précède complète les quatre concepts de base. Voici donc la question : si vous utilisez la souris pour cliquer sur un sous-élément b de l'élément a sur une page Web, le gestionnaire d'événements enregistré pour le sous-élément b doit-il être exécuté en premier ou le gestionnaire d'événements enregistré pour l'élément a doit-il être exécuté ? en premier (en supposant que l'élément a et son élément enfant b ont tous deux des gestionnaires d'événements enregistrés) ? En tant que lecteur, avez-vous déjà réfléchi à cette question ?
Ce problème concerne le mécanisme de propagation des événements dans le navigateur. Je crois que tout le monde a entendu parler des bulles d'événements et de la capture d'événements ! Oui, ils constituent le mécanisme de propagation des événements dans les navigateurs. Pas de photos, pas de vérité, pas de photos d'accompagnement ? Alors comment peut-on être généreux :
Après avoir lu l'image, je pense que vous avez une compréhension approximative du mécanisme de propagation des événements dans le navigateur : lorsqu'un événement se produit, il sera d'abord transmis depuis l'objet de niveau supérieur du navigateur, Window, vers celui qui a déclenché l'événement, c'est le processus de capture d'événement. Cependant, tout n'est pas encore terminé. L'événement est transmis depuis cet élément jusqu'à l'objet Window, qui est le processus de bouillonnement d'événements (mais dans IE8 et ses versions précédentes, le modèle d'événement ne définit pas de processus de capture, seulement un processus de bouillonnement).
Donc, concernant la question ci-dessus, cela dépend si le gestionnaire d'événements enregistré par l'élément a est dans le processus de capture ou dans le processus de bouillonnement. Alors, qu'est-ce que l'enregistrement d'un gestionnaire d'événements dans le processus de capture, et comment enregistrer un gestionnaire d'événements dans le processus de bouillonnement ? Parlons maintenant de plusieurs façons d'enregistrer des gestionnaires d'événements :
1. Définir les attributs des balises HTML en tant que gestionnaires d'événements
L'attribut du gestionnaire d'événements de l'élément document, son nom est composé de "on" suivi du nom de l'événement, par exemple : onclick, onmouseover. Bien entendu, ce formulaire ne peut enregistrer que des gestionnaires d'événements pour les éléments DOM. Exemple :
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1" onClick="console.log('div1');">div1 <div id="div2" oNClick="console.log('div2');">div2 <div id="div3" onclick="console.log('div3');" onclick="console.log('div3333');">div3 </div> </div> </div> <script type="text/javascript"> </script> </body> </html>
结果(鼠标点击div3区域后):
从结果中可以看出:
①因为HTML里面不区分大小写,所以这里事件处理程序属性名大写、小写、大小混写均可,属性值就是相应事件处理程序的JavaScript代码;
②若给同一元素写多个onclick事件处理属性,浏览器只执行第一个onclick里面的代码,后面的会被忽略;
③这种形式是在事件冒泡过程中注册事件处理程序的;
2.设置JavaScript对象属性为事件处理程序
可以通过设置某一事件目标的事件处理程序属性来为其注册相应的事件处理程序。事件处理程序属性名字由“on”后面跟着事件名组成,例如:onclick、onmouseover。实例:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.onclick = function(){ console.log('div1'); }; div2.onclick = function(){ console.log('div2'); }; div3.onclick = function(){ console.log('div3'); }; div1.onclick = function(){ console.log('div11111'); }; div1.onClick = function(){ console.log('DIV11111'); }; </script> </body> </html>
结果(鼠标点击div3区域后):
从结果中可以看出:
①因为JavaScript是严格区分大小写的,所以,这种形式下属性名只能按规定小写;
②若给同一元素对象写多个onclick事件处理属性,后面写的会覆盖前面的(ps:这就是在修改一个对象属性的值,属性的值是唯一确定的);
③这种形式也是在事件冒泡过程中注册事件处理程序的;
3.addEventListener()
前两种方式出现在Web初期,众多浏览器都有实现。而addEventListener()方法是标准事件模型中定义的。任何能成为事件目标的对象——这些对象包括Window对象、Document对象和所有文档元素等——都定义了一个名叫addEventListener()的方法,使用这个方法可以为事件目标注册事件处理程序。addEventListener()接受三个参数:第一个参数是要注册处理程序的事件类型,其值是字符串,但并不包括前缀“on”;第二个参数是指当指定类型的事件发生时应该调用的函数;第三个参数是布尔值,其可以忽略(某些旧的浏览器上不能忽略这个参数),默认值为false。这种情况是在事件冒泡过程中注册事件处理程序。当其为true时,就是在事件捕获过程中注册事件处理程序。实例:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.addEventListener('click', function(){ console.log('div1-bubble'); }, false); div2.addEventListener('click', function(){ console.log('div2-bubble'); }, false); div3.addEventListener('click', function(){ console.log('div3-bubble'); }, false); div3.addEventListener('click', function(){ console.log('div3-bubble222'); }, false); div1.addEventListener('click', function(){ console.log('div1-capturing'); }, true); div2.addEventListener('click', function(){ console.log('div2-capturing'); }, true); div3.addEventListener('click', function(){ console.log('div3-capturing'); }, true); </script> </body> </html>
结果(鼠标点击div3区域后):
从结果中可以看出:
①addEventListener()第三个参数的作用正如上面所说;
②通过addEventListener()方法给同一对象注册多个同类型的事件,并不会发生忽略或覆盖,而是会按顺序依次执行;
相对addEventListener()的是removeEventListener()方法,它同样有三个参数,前两个参数自然跟addEventListener()的意义一样,而第三个参数也只需跟相应的addEventListener()的第三个参数保持一致即可,同样可以省略,默认值为false。它表示从对象中删除某个事件处理函数。实例:
div1.addEventListener('click', div1BubbleFun, false); div1.removeEventListener('click', div1BubbleFun, false); function div1BubbleFun(){ console.log('div1-bubble'); }
4.attachEvent()
但是,IE8以及其之前版本的浏览器并不支持addEventListener()和removeEventListener()。相应的,IE定义了类似的方法attachEvent()和detachEvent()。因为IE8以及其之前版本浏览器也不支持事件捕获,所以attachEvent()并不能注册捕获过程中的事件处理函数,因此attachEvent()和detachEvent()要求只有两个参数:事件类型和事件处理函数。而且,它们的第一个参数使用了带“on”前缀的事件处理程序属性名。实例:
var div1 = document.getElementById('div1'); div1.attachEvent('onclick', div1BubbleFun); function div1BubbleFun(){ console.log('div1-bubble'); }
相应的,从对象上删除事件处理程序函数使用detachEvent()。例如:
div1.detachEvent('onclick', div1BubbleFun);
到此为止,我们已经说了浏览器中事件传播机制以及各种注册事件处理程序的方法。下面我们就再说说事件处理程序调用时的一些问题吧!
二.事件处理程序的调用
1.事件处理程序的参数:正如前面所说,通常事件对象作为参数传递给事件处理函数,但IE8以及其之前版本的浏览器中全局变量event才是事件对象。所以,我们在写相关代码时应该注意兼容性问题。实例(给页面上id为div1的元素添加点击事件,当点击该元素时在控制台输出事件类型和被点击元素本身):
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); if(div1.addEventListener){ div1.addEventListener('click', div1Fun, false); }else if(div1.attachEvent){ div1.attachEvent('onclick', div1Fun); } function div1Fun(event){ event = event || window.event; var target = event.target || event.srcElement; console.log(event.type); console.log(target); } </script> </body> </html>
2.事件处理程序的运行环境:关于事件处理程序的运行环境,也就是在事件处理程序中调用上下文(this值)的指向问题,可以看下面四个实例。
实例一:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> </script> </body> </html>
结果一:
从结果可以看出:
①第一种方法事件处理程序中this指向这个元素本身;
实例二:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; </script> </body> </html>
结果二:
从结果可以看出:
①第二种方法事件处理程序中this也指向这个元素本身;
②存在第二种方法时,它会覆盖第一种方法注册的事件处理程序;
实例三:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; div1.addEventListener('click', function(){ console.log('div1.addEventListener:'); console.log(this); }, false); </script> </body> </html>
结果三:
从结果可以看出:
①第三种方法事件处理程序中this也指向这个元素本身;
②第三种方法并不会覆盖第一种或第二种方法注册的事件处理程序;
实例四:
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; div1.attachEvent('onclick', function(){ console.log('div1.attachEvent:'); console.log(this === window); }); </script> </body> </html>
结果四:
从结果可以看出:
①第四种方法事件处理程序中this指向全局对象Window;
②第四种方法也不会覆盖第一种或第二种方法注册的事件处理程序;
3.事件处理程序的调用顺序:多个事件处理程序调用规则如下:
①通过HTML属性注册的处理程序和通过设置对象属性的处理程序一直优先调用;
②使用addEventListener()注册的处理程序按照它们的注册顺序依次调用;
③使用attachEvent()注册的处理程序可能按照任何顺序调用,所以代码不应该依赖于调用顺序;
4.事件取消:
①取消事件的浏览器默认操作(比如点击超链接元素会自动发生页面跳转的默认操作):如果使用前两种方法注册事件处理程序,可以在处理程序中添加返回值false来取消事件的浏览器默认操作。在支持addEventListener()的浏览器中,也可以通过调用事件对象的preventDefault()方法取消事件的默认操作。至于IE8及其之前的浏览器可以通过设置事件对象的returnValue属性为false来取消事件的默认操作。参考代码:
function cancelHandler(event){ var event = event || window.event; if(event.preventDefault){ event.preventDefault(); } if(event.returnValue){ event.returnValue = false; } return false; }
②取消事件传播:在支持addEventListener()的浏览器中,可以调用事件对象的一个stopPropagation()方法阻止事件的继续传播,它能工作在事件传播期间的任何阶段(捕获期阶段、事件目标本身、冒泡阶段);但是在IE8以及其之前版本的浏览器中并不支持stopPropagation()方法,而且这些浏览器也不支持事件传播的捕获阶段,相应的,IE事件对象有一个cancelBubble属性,设置这个属性为true能阻止事件进一步传播(即阻止其冒泡)。参考代码(阻止发生在div3区域的点击事件冒泡到div2和div1):
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.onclick = function(){ console.log('div1'); }; div2.onclick = function(){ console.log('div2'); }; div3.onclick = function(event){ stopEventPropagation(event); console.log('div3'); }; function stopEventPropagation(event){ var event = event || window.event; if(event.stopPropagation){ event.stopPropagation(); }else{ event.cancelBubble = true; } } </script> </body> </html>
Ce qui précède est une introduction complète au traitement des événements JavaScript. Bien sûr, il y a encore quelque chose à exploiter dans le bouillonnement d'événements. C'est ce que nous appelons souvent proxy d'événement ou délégation d'événement.