jQuery的事件命名空间详解
jquery现在的事件API:on,off,trigger支持带命名空间的事件,当事件有了命名空间,就可以有效地管理同一事件的不同监听器,在定义组件的时候,能够避免同一元素应用到不同组件时,同一事件类型之间的影响,还能控制一些意外的事件冒泡。在实际工作中,相信大家都用的很多,但是不一定了解它的所有细节,至少我有这样的经验,经常在碰到疑惑的时候,还得重新写例子去验证它的相关作用,所以本文想把事件命名空间相关的细节都梳理出来,将来再犯迷糊的时候可以回来翻着看看以便加深对它的理解和运用。
在详细了解命名空间之前,得先认识下什么是自定义事件,因为命名空间可以同时应用于自定义事件和浏览器默认事件当中。
1. 自定义事件
我们在定义组件的时候,浏览器的默认事件往往不能满足我们的要求,比如我们写了一个树形组件,它有一个实例方法init用来完成这个组件的初始化工作,在这个方法调用结束之后,我们通常会自定义一个init事件,以便外部可以在树组件初始化完成之后做一些回调处理:
<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这样的自定义事件,该代码运行结果如下
自定义事件的使用就跟浏览器默认事件的使用没有任何区别,就连事件冒泡和阻止事件默认行为都完全支持,唯一的区别在于:浏览器自带的事件类型可以通过浏览器的UI线程去触发,而自定义事件必须通过代码来手动触发:
2. 事件命名空间
事件命名空间类似css的类,我们在事件类型的后面通过点加名称的方式来给事件添加命名空间:
<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这个元素做实例化,以便实现一棵可以拖动的树:
<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这个元素定义了同一个名称的事件,所以后面实例化的组件在触发该事件的时候也会导致前面实例化的组件的同一事件再次被触发。通过命名空间就可以避免这个问题,让组件各自的事件回调互不影响:
<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次了:
3. 命名空间的匹配规则
在第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>
4. 命名空间的冒泡
为了说明命名空间的冒泡机制,需要把前面的测试代码改一改,并且以自定义事件来说明,测试代码如下:
<!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线程自动触发的,而且只要是浏览器自己触发的事件,是不会带命名空间的。这样的话,只要浏览器在子元素自动触发了默认事件,那么子元素以及父元素所有的监听器都会执行,有时候这并不一定是你期望的,所以最好在开发组件的时候始终加命名空间来触发或者添加监听,这样就能屏蔽掉浏览器自动触发给组件带来的影响。
5. 文中小结
通过第3和第4部分,可以发现jquery的事件机制,纵向是一种带命名空间的冒泡机制,横向是一种按照命名空间匹配规则的管理方式,如下图所示:
综合起来,一个元素上的某个事件监听器如果要被触发的话,一共有以下几种情况:
1)直接在该元素上触发了该事件,通过命名空间匹配规则被触发;
2)由子元素的相关事件冒泡到该元素,再通过匹配规则触发;
3)如果是浏览器默认事件,还会由浏览器自动触发,不过浏览器自动触发最终还是要体现到冒泡规则和匹配规则上来。
6. off方法中的使用
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这个命名空间的监听移除掉了