jquery現在的事件API:on,off,trigger支持帶命名空間的事件,當事件有了命名空間,就可以有效地管理同一事件的不同監聽器,在定義組件的時候,能夠避免同一元素應用到不同組件時,同一事件類型之間的影響,還能控制一些意外的事件冒泡。在實際工作中,相信大家都用的很多,但是不一定了解它的所有細節,至少我有這樣的經驗,經常在碰到疑惑的時候,還得重新寫例子去驗證它的相關作用,所以本文想把事件命名空間相關的細節都梳理出來,將來再犯迷糊的時候可以回來翻著看看以便加深對它的理解和運用。
在詳細了解命名空間之前,得先認識下什麼是自定義事件,因為命名空間可以同時應用於自定義事件和浏覽器默認事件當中。
我們在定義組件的時候,浏覽器的默認事件往往不能滿足我們的要求,比如我們寫了一個樹形組件,它有一個實例方法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線程去觸發,而自定義事件必須通過代碼來手動觸發:
事件命名空間類似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次了:
在第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這個命名空間的監聽移除掉了。
本文花了大量的測試去了解命名空間在事件觸發和事件冒泡以及移除監聽時候的特性,內容雖然非常之多,但是已經充分達到了本文的最終目的,就是要把命名空間在事件管理裡面的細節都梳理清楚,文中各個部分的核心內容最後都有簡短的結論,將來有需要的時候可以直接通過結論來解除自己的疑惑,希望能給大家帶來一些幫助,謝謝閱讀:)