1. イベント伝播メカニズム
クライアント側の JavaScript プログラム (つまり、ブラウザー) は、非同期のイベント駆動型プログラミング モデルを採用しています。 Web ブラウザは、ドキュメント、ブラウザ、要素、またはそれに関連するオブジェクトに何か興味深いことが起こったときにイベントを生成します。 JavaScript アプリケーションが特定の種類のイベントを考慮する場合、そのようなイベントが発生したときに呼び出される 1 つ以上の関数を登録できます。もちろん、このスタイルは Web プログラミングに固有のものではなく、グラフィカル ユーザー インターフェイスを使用するすべてのアプリケーションで採用されています。
イベント処理について詳しく説明したいので、いくつかの基本概念から始めましょう:
①イベントの種類:は、どのような種類のイベントが発生したかを記述するために使用される文字列です。たとえば、「mousemove」はユーザーがマウスを移動したことを意味し、「keydown」はキーボードのキーが押されたことを意味します。イベント タイプは単なる文字列であり、イベント名
と呼ばれることもあります。②イベント対象:は、イベントが発生する、またはイベントに関連するオブジェクトです。 Window、Document、Element オブジェクトは、最も一般的なイベント ターゲットです。もちろん、AJAX の XMLHttpRequest オブジェクトもイベント ターゲットです。
③イベントハンドラ:はイベントを処理したり応答したりする関数で、イベントリスナーとも呼ばれます。アプリケーションは、イベント タイプとイベント ターゲットを指定して、Web ブラウザにイベント ハンドラーを登録します。
④イベントオブジェクト:は、特定のイベントに関連し、イベントに関する詳細情報を含むオブジェクトです。イベント オブジェクトはパラメーターとしてイベント処理関数に渡されます (ただし、IE8 以前のバージョンでは、グローバル変数event がイベント オブジェクトです)。イベント オブジェクトには、イベントの種類を指定する type 属性と、イベントのターゲットを指定する target 属性があります (ただし、IE8 以前のバージョンでは、target の代わりに srcElement が使用されます)。もちろん、さまざまなタイプのイベントでは、関連するイベント オブジェクトの他の固有のプロパティも定義されます。たとえば、マウス イベントの関連オブジェクトにはマウス ポインターの座標が含まれ、キーボード イベントの関連オブジェクトには押されたキーと修飾キーの詳細が含まれます。
以上で 4 つの基本概念が完成しました。ここで疑問が生じます。Web ページ上の要素 a のサブ要素 b をマウスを使用してクリックした場合、サブ要素 b に登録されているイベント ハンドラーが最初に実行されるべきか、それとも要素 a に登録されているイベント ハンドラーが実行されるべきですか。まず (要素 a とその子要素 b の両方にイベント ハンドラーが登録されていると仮定します)?読者として、この質問について考えたことはありますか?この問題には、ブラウザのイベント伝播メカニズムが関係しています。イベントバブルやイベントキャプチャーというのは皆さんも聞いたことがあると思います!はい、これらはブラウザのイベント伝播メカニズムです。写真も真実も添付写真もありませんか?では、どうすれば寛大になれるのでしょうか:
この図を読んだ後、ブラウザのイベント伝播メカニズムについて大まかに理解できたと思います。イベントが発生すると、イベントはまずブラウザの最上位オブジェクトである Window から、そのオブジェクトに渡されます。イベントをトリガーした要素、これがイベント キャプチャ プロセスです。ただし、まだすべてが終わったわけではありません。イベントはこの要素からイベント バブリング プロセスである Window オブジェクトまで渡されます (ただし、IE8 およびその以前のバージョンでは、イベント モデルはキャプチャ プロセスを定義するだけです)。発泡プロセス)。
ということで、上記の質問ですが、要素aで登録されたイベントハンドラがキャプチャ処理中かバブリング処理中かによって異なります。では、キャプチャ プロセスにイベント ハンドラーを登録するとは具体的に何でしょうか。また、バブリング プロセスにイベント ハンドラーを登録する方法は何でしょうか?次に、イベント ハンドラーを登録するいくつかの方法について説明します。
1. HTML タグの属性をイベント ハンドラーとして設定します
ドキュメント要素のイベント ハンドラー属性。その名前は、「on」とそれに続くイベント名で構成されます (例: onclick、onmouseover)。もちろん、このフォームでは DOM 要素のイベント ハンドラーのみを登録できます。例:
<!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>
上記は JavaScript イベント処理の完全な紹介です。もちろん、イベント プロキシまたはイベント デリゲーションと呼ばれるものについては、活用できる点がまだあります。この知識は将来更新される予定です。