jquery如今的事件API:on,off,trigger支持带命名空间的事件,当事件有了命名空间,就能够有效地管理同一事件的不一样监听器,在定义组件的时候,可以避免同一元素应用到不一样组件时,同一事件类型之间的影响,还能控制一些意外的事件冒泡。在实际工做中,相信你们都用的不少,可是不必定了解它的全部细节,至少我有这样的经验,常常在碰到疑惑的时候,还得从新写例子去验证它的相关做用,因此本文想把事件命名空间相关的细节都梳理出来,未来再犯迷糊的时候能够回来翻着看看以便加深对它的理解和运用。css
在详细了解命名空间以前,得先认识下什么是自定义事件,由于命名空间能够同时应用于自定义事件和浏览器默认事件当中。html
咱们在定义组件的时候,浏览器的默认事件每每不能知足咱们的要求,好比咱们写了一个树形组件,它有一个实例方法init用来完成这个组件的初始化工做,在这个方法调用结束以后,咱们一般会自定义一个init事件,以便外部能够在树组件初始化完成以后作一些回调处理:java
<script src="../js/lib/jquery.js"></script> <div id="tree"> </div> <script> var Tree = function(element, options) { var $tree = this.$tree = $(element); //监听init事件,触发 $tree.on('init', $.proxy(options.onInit, this)); this.init(); }; Tree.prototype.init = function() { console.log('tree init!'); this.$tree.trigger('init'); }; var tree = new Tree('#tree', { onInit: function() { console.log(this.$tree.outerHeight()); } }); </script>
以上代码中.on('init',…)中的init就是一个相似click这样的自定义事件,该代码运行结果以下jquery
自定义事件的使用就跟浏览器默认事件的使用没有任何区别,就连事件冒泡和阻止事件默认行为都彻底支持,惟一的区别在于:浏览器自带的事件类型能够经过浏览器的UI线程去触发,而自定义事件必须经过代码来手动触发: 浏览器
事件命名空间相似css的类,咱们在事件类型的后面经过点加名称的方式来给事件添加命名空间:app
<script> var Tree = function(element, options) { var $tree = this.$tree = $(element); //监听init事件,触发 $tree.on('init.my.tree', $.proxy(options.onInit, this)); this.init(); }; Tree.prototype.init = function() { console.log('tree init!'); this.$tree.trigger('init.my.tree'); }; var tree = new Tree('#tree', { onInit: function() { console.log(this.$tree.outerHeight()); } }); </script>
以上代码中.on('init.my.tree',…)经过.my和.tree给init这个事件添加了2个命名空间,注意命名空间是相似css的类,而不是相似java中的package,因此这两个命名空间的名称分别是.my和.tree,而不是my和my.tree,注意命名空间的名称前面必定要带点,这个名称在off的时候能够用到。在监听和触发事件的时候带上命名空间,当触发带命名空间的事件时,只会调用匹配该命名空间的监听器。因此命名空间能够有效地管理同一事件的不一样监听器,尤为在定义组件的时候能够有效地保证组件内部的事件只在组件内部有效,不会影响到其它组件。测试
如今假设咱们不用命名空间,同时定义两个组件Tree和Dragable,而且同时对#tree这个元素作实例化,以便实现一棵能够拖动的树:this
<script> var Tree = function(element, options) { var $tree = this.$tree = $(element); //监听init事件,触发 $tree.on('init', $.proxy(options.onInit, this)); this.init(); }; Tree.prototype.init = function() { console.log('tree init!'); this.$tree.trigger('init'); }; var tree = new Tree('#tree', { onInit: function() { console.log(this.$tree.outerHeight()); } }); var Dragable = function(element, options) { var $element = this.$element = $(element); //监听init事件,触发 $element.on('init', $.proxy(options.onInit, this)); this.init(); }; Dragable.prototype.init = function() { console.log('tree init!'); this.$element.trigger('init'); }; var drag = new Dragable('#tree', { onInit: function() { console.log('start drag!'); } }); </script>
结果会发现Tree的onInit回调被调用两次:
根本缘由就是由于#tree这个元素被应用到了多个组件,在这两个组件内部对#tree这个元素定义了同一个名称的事件,因此后面实例化的组件在触发该事件的时候也会致使前面实例化的组件的同一事件再次被触发。经过命名空间就能够避免这个问题,让组件各自的事件回调互不影响:spa
<script> var Tree = function(element, options) { var $tree = this.$tree = $(element); //监听init事件,触发 $tree.on('init.my.tree', $.proxy(options.onInit, this)); this.init(); }; Tree.prototype.init = function() { console.log('tree init!'); this.$tree.trigger('init.my.tree'); }; var tree = new Tree('#tree', { onInit: function() { console.log(this.$tree.outerHeight()); } }); var Dragable = function(element, options) { var $element = this.$element = $(element); //监听init事件,触发 $element.on('init.my.dragable', $.proxy(options.onInit, this)); this.init(); }; Dragable.prototype.init = function() { console.log('drag init!'); this.$element.trigger('init.my.dragable'); }; var drag = new Dragable('#tree', { onInit: function() { console.log('start drag!'); } }); </script>
这样tree实例的onInit就不会被调用2次了:prototype
在第2部分的举例当中,触发带命名空间的事件时,触发方式是:
而后就会调用这里监听的回调:
若是把触发方式改一下,不改监听方式,改为如下三种方式的一种,结果会怎么样呢:
this.$element.trigger('init'); this.$element.trigger('init.dragable'); this.$element.trigger('init.my');
答案是该监听回调依然会被调用。这个跟命名空间的匹配规则有关,为了说明这个规则,能够用如下的这个代码来测试:
<!DOCTYPE html> <html lang="en"> <head> <title>xxxxx</title> <style type="text/css"> #parent { margin: 100px auto 0 auto; width: 600px; height: 200px; border: 1px solid #ccc; position: relative; } .log { position: absolute; left: 0; width: 100%; height: 100%; overflow: auto; } p { margin: 0; } #btns { margin: 20px auto 0 auto; width: 600px; } </style> </head> <body> <script src="../js/lib/jquery.js"></script> <div id="parent"> <div class="log"></div> </div> <div id="btns"> <button id="btn1" type="button" onclick="$p.trigger('click.n1.n2.n3.n4');">trigger('click.n1.n2.n3.n4')</button> <button id="btn2" type="button" onclick="$p.trigger('click.n1.n2.n3');">trigger('click.n1.n2.n3')</button> <button id="btn3" type="button" onclick="$p.trigger('click.n1.n2');">trigger('click.n1.n2')</button> <button id="btn4" type="button" onclick="$p.trigger('click.n1');">trigger('click.n1')</button> <button id="btn5" type="button" onclick="$p.trigger('click');">trigger('click')</button> </div> <script> function log($e, msg) { var $log = $e.find('.log'); $log.append('<p>' + msg + '</p>'); } var $p = $('#parent'); $p.on('click.n1.n2.n3.n4', function(){ log($p, 'click n1 n2 n3 n4'); }); $p.on('click.n1.n2.n3', function(){ log($p, 'click n1 n2 n3'); }); $p.on('click.n1.n2', function(){ log($p, 'click n1 n2'); }); $p.on('click.n1', function(){ log($p, 'click n1'); }); $p.on('click', function(){ log($p, 'click'); }); </script> </body> </html>
初始化效果以下:
依次点击界面上的按钮(不过点击按钮前得先刷新页面,这样的话各个按钮效果才不会混在一块儿),界面打印的效果以下:
以上的测试代码一共给$p元素的click事件定义了4个命名空间,而后针对不一样的命名空间数量,添加了五个监听器,经过外部的按钮来手动触发各个带命名空间的事件,从最后的结果,咱们能得出这样一个规律:
1)当触发不带命名空间的事件时,该事件全部的监听器都会触发;(从最后一个按钮的测试结果可看出)
2)当触发带一个命名空间的事件时,在监听时包含该命名空间的全部监听器都会被触发;(从第4个按钮的测试结果可看出)
3)当触发带多个命名空间的事件时,只有在监听时同时包含那多个命名空间的监听器才会被触发;(从第2,3个按钮的测试结果可看出)
4)只要触发带命名空间的事件,该事件不带命名空间的监听器就不会被触发;(从1,2,3,4个按钮可看出)
5)2跟3其实就是一个,2是3的一种状况
这个规律彻底适用于浏览器默认事件和自定义事件,自定义事件的测试能够用下面的代码,结论是一致的:
<!DOCTYPE html> <html lang="en"> <head> <title>xxxxx</title> <style type="text/css"> #parent { margin: 100px auto 0 auto; width: 600px; height: 200px; border: 1px solid #ccc; position: relative; } .log { position: absolute; left: 0; width: 100%; height: 100%; overflow: auto; } p { margin: 0; } #btns { margin: 20px auto 0 auto; width: 600px; } </style> </head> <body> <script src="../js/lib/jquery.js"></script> <div id="parent"> <div class="log"></div> </div> <div id="btns"> <button id="btn1" type="button" onclick="$p.trigger('hello.n1.n2.n3.n4');">trigger('hello.n1.n2.n3.n4')</button> <button id="btn2" type="button" onclick="$p.trigger('hello.n1.n2.n3');">trigger('hello.n1.n2.n3')</button> <button id="btn3" type="button" onclick="$p.trigger('hello.n1.n2');">trigger('hello.n1.n2')</button> <button id="btn4" type="button" onclick="$p.trigger('hello.n1');">trigger('hello.n1')</button> <button id="btn5" type="button" onclick="$p.trigger('hello');">trigger('hello')</button> </div> <script> function log($e, msg) { var $log = $e.find('.log'); $log.append('<p>' + msg + '</p>'); } var $p = $('#parent'); $p.on('hello.n1.n2.n3.n4', function(){ log($p, 'hello n1 n2 n3 n4'); }); $p.on('hello.n1.n2.n3', function(){ log($p, 'hello n1 n2 n3'); }); $p.on('hello.n1.n2', function(){ log($p, 'hello n1 n2'); }); $p.on('hello.n1', function(){ log($p, 'hello n1'); }); $p.on('hello', function(){ log($p, 'hello'); }); </script> </body> </html>
为了说明命名空间的冒泡机制,须要把前面的测试代码改一改,而且以自定义事件来讲明,测试代码以下:
<!DOCTYPE html> <html lang="en"> <head> <title>xxxxx</title> <style type="text/css"> #parent { margin: 100px auto 0 auto; width: 600px; height: 300px; border: 1px solid #ccc; position: relative; } #child { margin: 0 0 0 300px; width: 300px; height: 300px; border: 1px solid #ccc; position: relative; } .log { position: absolute; left: 0; width: 100%; height: 100%; overflow: auto; } p { margin: 0; } #btns { margin: 20px auto 0 auto; width: 600px; } </style> </head> <body> <script src="../js/lib/jquery.js"></script> <div id="parent"> <div class="log"></div> <div id="child"> <div class="log"></div> </div> </div> <div id="btns"> <button id="btn1" type="button" onclick="$c.trigger('hello.n1.n2.n3.n4');">trigger('hello.n1.n2.n3.n4')</button> <button id="btn2" type="button" onclick="$c.trigger('hello.n1.n2.n3');">trigger('hello.n1.n2.n3')</button> <button id="btn3" type="button" onclick="$c.trigger('hello.n1.n2');">trigger('hello.n1.n2')</button> <button id="btn4" type="button" onclick="$c.trigger('hello.n1');">trigger('hello.n1')</button> <button id="btn5" type="button" onclick="$c.trigger('hello');">trigger('hello')</button> </div> <script> function log($e, msg) { var $log = $e.children('.log'); $log.append('<p>' + msg + '</p>'); } var $p = $('#parent'); $p.on('hello.n1.n2.n3.n4', function(){ log($p, 'hello n1 n2 n3 n4'); }); $p.on('hello.n1.n2.n3', function(){ log($p, 'hello n1 n2 n3'); }); $p.on('hello.n1.n2', function(){ log($p, 'hello n1 n2'); }); $p.on('hello.n1', function(){ log($p, 'hello n1'); }); $p.on('hello', function(){ log($p, 'hello'); }); var $c = $('#child'); $c.on('hello.n1.n2.n3.n4', function(){ log($c, 'hello n1 n2 n3 n4'); }); $c.on('hello.n1.n2.n3', function(){ log($c, 'hello n1 n2 n3'); }); $c.on('hello.n1.n2', function(){ log($c, 'hello n1 n2'); }); $c.on('hello.n1', function(){ log($c, 'hello n1'); }); $c.on('hello', function(){ log($c, 'hello'); }); </script> </body> </html>
初始化效果以下:
在这个测试中,点击按钮的时候触发的并非$p元素的事件,而是$c元素的事件,$p是$c的父元素,上图中整个长方形容器就是$p元素,右边的正方形容器就是$c元素。当咱们依次点击上面五个按钮的时候(仍是采起刷新一次点一个按钮的方式),界面打印的效果以下:
从这个测试结果来看,咱们能够得出一个结论:jquery提供的事件机制,当子元素的带命名空间的事件冒泡到父级元素时,会以一样的命名空间触发父级元素的同一事件,为了方便起见,能够把这种冒泡机制称为带命名空间的冒泡。意味着当子元素的事件冒泡到父级元素时,只有那些知足该事件匹配规则的父级监听器才会被调用。
浏览器默认事件的冒泡也与自定义事件的机制相同,能够用下面的代码测试:
<!DOCTYPE html> <html lang="en"> <head> <title>xxxxx</title> <style type="text/css"> #parent { margin: 100px auto 0 auto; width: 600px; height: 300px; border: 1px solid #ccc; position: relative; } #child { margin: 0 0 0 300px; width: 300px; height: 300px; border: 1px solid #ccc; position: relative; } .log { position: absolute; left: 0; width: 100%; height: 100%; overflow: auto; } p { margin: 0; } #btns { margin: 20px auto 0 auto; width: 600px; } </style> </head> <body> <script src="../js/lib/jquery.js"></script> <div id="parent"> <div class="log"></div> <div id="child"> <div class="log"></div> </div> </div> <div id="btns"> <button id="btn1" type="button" onclick="$c.trigger('click.n1.n2.n3.n4');">trigger('click.n1.n2.n3.n4')</button> <button id="btn2" type="button" onclick="$c.trigger('click.n1.n2.n3');">trigger('click.n1.n2.n3')</button> <button id="btn3" type="button" onclick="$c.trigger('click.n1.n2');">trigger('click.n1.n2')</button> <button id="btn4" type="button" onclick="$c.trigger('click.n1');">trigger('click.n1')</button> <button id="btn5" type="button" onclick="$c.trigger('click');">trigger('click')</button> </div> <script> function log($e, msg) { var $log = $e.children('.log'); $log.append('<p>' + msg + '</p>'); } var $p = $('#parent'); $p.on('click.n1.n2.n3.n4', function(){ log($p, 'click n1 n2 n3 n4'); }); $p.on('click.n1.n2.n3', function(){ log($p, 'click n1 n2 n3'); }); $p.on('click.n1.n2', function(){ log($p, 'click n1 n2'); }); $p.on('click.n1', function(){ log($p, 'click n1'); }); $p.on('click', function(){ log($p, 'click'); }); var $c = $('#child'); $c.on('click.n1.n2.n3.n4', function(){ log($c, 'click n1 n2 n3 n4'); }); $c.on('click.n1.n2.n3', function(){ log($c, 'click n1 n2 n3'); }); $c.on('click.n1.n2', function(){ log($c, 'click n1 n2'); }); $c.on('click.n1', function(){ log($c, 'click n1'); }); $c.on('click', function(){ log($c, 'click'); }); </script> </body> </html>
须要特别注意的是:浏览器的默认事件能经过鼠标或键盘等操做,由浏览器UI线程自动触发的,并且只要是浏览器本身触发的事件,是不会带命名空间的。这样的话,只要浏览器在子元素自动触发了默认事件,那么子元素以及父元素全部的监听器都会执行,有时候这并不必定是你指望的,因此最好在开发组件的时候始终加命名空间来触发或者添加监听,这样就能屏蔽掉浏览器自动触发给组件带来的影响。
经过第3和第4部分,能够发现jquery的事件机制,纵向是一种带命名空间的冒泡机制,横向是一种按照命名空间匹配规则的管理方式,以下图所示:
综合起来,一个元素上的某个事件监听器若是要被触发的话,一共有如下几种状况:
1)直接在该元素上触发了该事件,经过命名空间匹配规则被触发;
2)由子元素的相关事件冒泡到该元素,再经过匹配规则触发;
3)若是是浏览器默认事件,还会由浏览器自动触发,不过浏览器自动触发最终仍是要体现到冒泡规则和匹配规则上来。
jquery中在移除事件监听的时候,有多种方式,能够不带命名空间只经过事件类型来移除:
$p.off('click');
也能够经过带命名空间的事件类型来移除:
$p.off('click.n1');
还能够只按命名空间来移除:
$p.off('.n1');
为了更清楚地说明这三种移除方式的效果和规律,能够如下代码来测试
<!DOCTYPE html> <html lang="en"> <head> <title>xxxxx</title> <style type="text/css"> #parent { margin: 100px auto 0 auto; width: 600px; height: 300px; border: 1px solid #ccc; position: relative; } .log { position: absolute; left: 0; width: 100%; height: 100%; overflow: auto; } p { margin: 0; } #btns { margin: 20px auto 0 auto; width: 600px; } </style> </head> <body> <script src="../js/lib/jquery.js"></script> <div id="parent"> <div class="log"></div> </div> <div id="btns"> <button id="btn1" type="button" onclick="$p.off('click.n1.n2.n3.n4');">off('click.n1.n2.n3.n4')</button> <button id="btn2" type="button" onclick="$p.off('click.n1.n2.n3');">off('click.n1.n2.n3')</button> <button id="btn3" type="button" onclick="$p.off('click.n1.n2');">off('click.n1.n2')</button> <button id="btn4" type="button" onclick="$p.off('click.n1');">off('click.n1')</button> <button id="btn5" type="button" onclick="$p.off('click');$p.trigger('hello');">off('click')</button> <button id="btn6" type="button" onclick="$p.off('.n1.n2.n3.n4');">off('.n1.n2.n3.n4')</button> <button id="btn7" type="button" onclick="$p.off('.n1.n2.n3');">off('.n1.n2.n3')</button> <button id="btn8" type="button" onclick="$p.off('.n1.n2');">off('.n1.n2')</button> <button id="btn9" type="button" onclick="$p.off('.n1');">off('.n1')</button> </div> <script> function log($e, msg) { var $log = $e.children('.log'); $log.append('<p>' + msg + '</p>'); } var $p = $('#parent'); $p.on('click.n1.n2.n3.n4', function(){ log($p, 'click n1 n2 n3 n4'); }); $p.on('click.n1.n2.n3', function(){ log($p, 'click n1 n2 n3'); }); $p.on('click.n1.n2', function(){ log($p, 'click n1 n2'); }); $p.on('click.n1', function(){ log($p, 'click n1'); }); $p.on('click', function(){ log($p, 'click'); $p.trigger('hello'); }); $p.on('hello.n1.n2.n3.n4', function(){ log($p, 'hello n1 n2 n3 n4'); }); $p.on('hello.n1.n2.n3', function(){ log($p, 'hello n1 n2 n3'); }); $p.on('hello.n1.n2', function(){ log($p, 'hello n1 n2'); }); $p.on('hello.n1', function(){ log($p, 'hello n1'); }); $p.on('hello', function(){ log($p, 'hello'); }); </script> </body> </html>
初始化界面效果为:
在这个测试中,为$p元素的两种事件click和hello各添加了五个监听器,命名空间的的设置还与前面的相似,hello事件在click事件不带命名空间的回调里被触发,提供了9个按钮分别用来测试不一样的off事件的方式最后的结果。测试的方法是依次点击按钮(为了避免让各个测试的结果互相影响,点击前仍是得先刷新页面),点完按钮后,再点击一下$p元素,就是那个灰色边框的容器。只有第五个按钮不须要作第二次$p元素的点击,由于它已经把$p的click事件监听所有移除了,各个按钮的测试结果以下:
结果:click.n1.n2.n3.n4的监听没有被调用,hello事件不受影响。
结果:click.n1.n2.n3.n4和click.n1.n2.n3的监听没有被调用,hello事件不受影响。
结果:click.n1.n2.n3.n4和click.n1.n2.n3和click.n1.n2的监听没有被调用,hello事件不受影响。
结果:click.n1.n2.n3.n4和click.n1.n2.n3和click.n1.n2和click.n1的监听没有被调用,hello事件不受影响。
结果:全部click事件的回调都没有调用,hello事件不受影响。
综合以上的测试结果,能够得出的结论是:
1)当经过一个或多个命名空间结合事件类型来移除的时候,只会把该事件的在添加监听的时候包含那些命名空间的监听器移除,不会影响该事件类型的其它监听器以及其它事件类型。好比移除click.n1.n2,会把click.n1.n2,click.n1.n2.n3还有click.n1.n2.n3.n4都移除,可是click.n1 , click 还有hello事件都不受影响。
2)当经过事件类型来移除的时候,会把该事件的全部监听器都移除。
再看从第6个按钮开始的测试:
结果:移除了click.n1.n2.n3.n4和hello.n1.n2.n3.n4,其它事件监听不受影响。
结果:移除了click.n1.n2.n3.n4,click.n1.n2.n3和hello.n1.n2.n3.n4,hello.n1.n2.n3,其它事件监听不受影响。
结果:移除了click.n1.n2.n3.n4,click.n1.n2.n3,click.n1.n2和hello.n1.n2.n3.n4,hello.n1.n2.n3,hello.n1.n2,其它事件监听不受影响。
结果:移除了hello和click事件全部的带命名空间的监听。
综合最后这部分的测试结果,能够得出的结论是:
经过命名空间移除监听的时候,会影响全部的事件类型,会把全部事件类型的在添加监听的时候包含那些命名空间的监听器所有移除掉。好比最后的off(.n1),就把click和hello事件的全部带.n1这个命名空间的监听移除掉了。
本文花了大量的测试去了解命名空间在事件触发和事件冒泡以及移除监听时候的特性,内容虽然很是之多,可是已经充分达到了本文的最终目的,就是要把命名空间在事件管理里面的细节都梳理清楚,文中各个部分的核心内容最后都有简短的结论,未来有须要的时候能够直接经过结论来解除本身的疑惑,但愿能给你们带来一些帮助,谢谢阅读:)