阻止事件冒泡和围绕.on()方法 [.selector]事件委托中冒泡情况的探讨
一、事件冒泡和阻止事件冒泡
事件冒泡是JavaScript的核心概念之一,它的原理很简单,但真正应用起来还是有不少的坑。
这里说一说关于阻止事件冒泡的本质。
我们都知道阻止事件冒泡的方法:
1. event.stopPropagation(),停止冒泡。
2.在事件触发的执行函数Handler中写return false,对事件对象停止冒泡和停止默认行为。
需要注意:
event.stopPropagation()停止事件冒泡,阻止从绑定的元素向上冒泡。
$('#main').on('click', 'li', function () {
// do somthing
event.stopPropagation();
}) ;
click事件冒泡到#main就不再向上冒泡,并不是li往上不冒泡。
而return false,实际上是终结了这个(点击)事件,冒泡当然也就停止了。在后述的测试中会分析。
二、 .on()方法的 [.selector]
.on( events [,selector ] [, data ], handler(eventObject) )
jQuery API中对.on()方法的定义:
.on()方法事件处理程序到当前选定的jQuery对象中的元素。在jQuery 1.7中,.on()方法 提供绑定事件处理的所有功能。为了帮助从旧的jQuery事件方法转换过来,查看 .bind(), .delegate(), 和 .live()。
2.1 不加入[.selector],相当于以前的.bind(),简单粗暴直接绑定事件
文档:如果省略selector
或者是null,那么事件处理程序被称为直接事件 或者 直接绑定事件 。每次选中的元素触发事件时,就会执行处理程序,不管它直接绑定在元素上,还是从后代(内部)元素冒泡到该元素的。
这要分两种情况:
(1)当要在目标元素上直接触发事件,而不需要事件委托时,那么直接用目标元素调用.on(),例如:
$(‘div’).on(‘click’,func);
(2)需要进行事件委托。
例如点击ul列表中的li触发事件,应当采用事件委托。那么绑定的元素可以是li的父元素ul,或者是body、document。
以绑定ul为例:
$('ul').on('click', function () {
// do something;
});
当不加入[.selector],那么点击ul以及ul之下的任意元素都会触发事件。
但通常情况下,我们进行事件委托都是要委托给绑定元素之一的一个特定元素,而不是委托给任意一个元素。所以可以不考虑采用这种方式,而是下面的。
2.2 加入[.selector],真正意义上的事件委托
文档:当提供selector
参数时,事件处理程序是指为委派 事件(愚人码头注:通常也有很多人叫它代理事件)。事件不会在直接绑定的元素上触发,但当selector
参数选择器匹配到后代(内部元素)的时候,事件处理函数才会被触发。jQuery会从
event.target 开始向上层元素(例如,由最内层元素到最外层元素)开始冒泡,并且在传播路径上所有绑定了相同事件的元素若满足匹配的选择器,那么这些元素上的事件也会被触发。
——加入[.selector]的作用实际上是规范了事件委托。
例如点击li触发事件,li的父元素ul,或者是body、document,就可写为:
$('ul').on('click', 'li', function () {
// do something;
});
注意文档中标红的文字,可以这样理解:
当selector参数选择器匹配的内部元素(这里是li)及其后代的元素被点击,意味着传播路径经过了这个匹配的元素,事件得以触发。(注意不是只有li被点击会触发,因为冒泡传播,其后代的元素被点击也会触发事件。)
我们通过一个测试,来验证上述理论,并从中发现一些规则。我们测试的是点击最外层盒子'#propa'本身及其后代的元素,触发事件的情况,着重考虑点击li的情况。
一、不含选择器直接绑定,带有事件委托功能
$('#propa').on('click', function () {
console.log(event.target);
});
点击1,2,3,4,5,7,8,9都会输出
因为冒泡,点击最外层盒子'#propa'之内的任意元素都会触发事件
二、采取事件委托。含选择器‘li’。
$('#propa').on('click', 'li', function () {
console.log(event.target);
});
点击1,2,3会输出,li和li的后代元素触发
因为事件委托,冒泡从event.target直至#propa,只有传播过程中遇到li才会触发,包括点击li和li的后代元素。
三-1、用'#propa'事件委托. 为’li’和’li’下面的第一代div匹配事件,针对$('#propa').on使用event.stopPropagation()
$('#propa').on('click', '.li-div', function () {
console.log(event.target);
event.stopPropagation();
});
$('#propa').on('click', 'li', function () {
console.log('prop triggered!');
});
点击1会输出如下,冒泡从event.target至#propa 。为li-div和li匹配的事件都触发。
点击2会输出如下,冒泡从event.target至#propa 。为li-div和li匹配的事件都触发。
点击3会输出如下,冒泡从event.target至#propa 。只匹配到li。
因为事件委托,冒泡从event.target直至#propa。先匹配到更内层的选择器(li-div),执行第1个函数,再匹配到外层的选择器(li),执行第2个函数。使用event.stopPropagation()阻止的是从最外层盒子‘#propa’向上冒泡。
三-2、用'#propa'事件委托. 为’li’和’li’下面的第一代div匹配事件,针对$('#propa').on使用return false
$('#propa').on('click', '.li-div', function () {
console.log(event.target);
return false;
});
$('#propa').on('click', 'li', function () {
console.log('proptriggered!');
点击1会输出如下,冒泡从event.target至#propa ,为li-div匹配的事件触发,然后return false立即结束点击事件,不触发为 li匹配的事件。
点击2会输出如下,冒泡从event.target至#propa ,为li-div匹配的事件触发,然后return false立即结束点击事件,不触发为 li匹配的事件。
点击3会输出如下,冒泡从event.target至#propa 。只匹配到li。
因为事件委托,冒泡从event.target直至#propa。先匹配到更内层的选择器(li-div),执行第1个函数,执行到return false语句后,立即结束点击事件,不会继续向上匹配,所以不执行第2个函数。
四-1、分别用'#propa'直接绑定和事件委托。事件委托为’li’匹配事件。针对 $('#propa')的直接绑定使用 event.stopPropagation()
$('#propa').on('click', function () {
console.log(event.target);
event.stopPropagation();
});
$('#propa').on('click', 'li', function () {
console.log('proptriggered!');
});
点击1会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发。
点击2会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发
点击3会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发
点击4、5会输出如下,冒泡从event.target至#propa,'#propa'直接绑定的事件触发
因为事件委托,冒泡从event.target直至#propa。为li匹配的事件触发,同时为#propa绑定两个事件,含有事件委托的先执行,直接绑定的后执行。
四-2、分别用'#propa'直接绑定和事件委托。事件委托为’li’匹配事件。针对 $('#propa')的直接绑定使用return false
$('#propa').on('click', function () {
console.log(event.target);
return false;
});
$('#propa').on('click', 'li', function () {
console.log('proptriggered!');
点击1会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发,最后return false结束点击事件。
点击2会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发,最后return false结束点击事件
点击3会输出如下,冒泡从event.target至#propa 。为li匹配的事件先触发,然后是'#propa'直接绑定的事件触发,最后return false结束点击事件
点击4、5会输出如下,冒泡从event.target至#propa,'#propa'直接绑定的事件触发。
因为事件委托,冒泡从event.target直至#propa。为li匹配的事件触发,同时为#propa绑定两个事件,含有事件委托的先执行,直接绑定的后执行,return false最后才发生,不会影响两个函数的执行。
五-1、用body事件委托. 为’li’和’li’下面的第一代div匹配事件,针对$('body').on使用event.stopPropagation()。用#propa事件委托,为’li’匹配事件。
$('body ').on('click', '.li-div', function () {
console.log(event.target);
event.stopPropagation();
});
$('#propa').on('click', 'li', function () {
console.log('prop triggered!');
});
点击1会输出如下,先是冒泡从event.target至#propa ,为li匹配的事件先触发,然后冒泡至body,为li的第一代div匹配的事件触发。
点击2会输出如下,先是冒泡从event.target至#propa ,为li匹配的事件先触发,然后冒泡至body,为li的第一代div匹配的事件触发。
点击3会输出如下,冒泡从event.target至body,只匹配到li
使用event.stopPropagation()结束为body绑定的事件。
五-2、用body事件委托,为li’下面的第一代div匹配事件,针对$('body ').on使用return false。用#propa事件委托,为’li’匹配事件。
$('body ').on('click', '.li-div', function () {
console.log(event.target);
return false;
});
$('#propa').on('click', 'li', function () {
console.log('proptriggered!');
});
点击1会输出如下,先是冒泡从event.target至#propa ,为li匹配的事件先触发,然后冒泡至body,为li的第一代div匹配的事件触发,最后return false
点击2会输出如下,先是冒泡从event.target至#propa ,为li匹配的事件先触发,然后冒泡至body,为li的第一代div匹配的事件触发,最后return false
点击3会输出如下,冒泡从event.target至body,只匹配到li
为body绑定的事件函数中使用return false结束点击事件
六、含选择器‘a’
$('#propa').on('click', 'a', function () {
console.log('a is triggered!')
});
点击8和9输出相同内容
七、含选择器‘img’
$('#propa').on('click', 'img', function () {
console.log('img is triggered!')
})
点击9输出,点击8不输出
根据测试,得到绑定元素、选择器[,selector]、阻止冒泡方式(event.stopPropagation()或者return false)与事件处理队列和触发情况的关系。
最后,我们得出以下结论:
1. 事件冒泡是从内部event.target直至绑定的元素,event.stopPropagation()是阻止冒泡从绑定元素再往上传播;return false是执行完当前事件函数后立即终止该事件类型,冒泡、默认处理、具有相同事件类型的其它绑定事件都不再生效。
2. 事件执行的优先级
为不同元素绑定的事件,优先执行靠内层元素绑定的事件;
同一元素绑定的多个事件,优先执行事件委托(含有选择器),再执行直接绑定事件。
3. 根据上述1、2条,event.stopPropagation()不会影响内部event.target到绑定元素之间的冒泡传播过程。
同一事件类型的多个绑定,如果return false在先执行的事件处理函数终,会终结尚未执行的事件绑定(当然也终止了冒泡);如果在最后执行的事件处理函数中,不会影响冒泡传播过程。
4. 慎用外层元素(body,document等).on() +[,selector]的方式。当希望利用冒泡进行事件委托时,宜采用(body,document等).on() + [,selector]的方式,要注意冒泡会在一个很长的路径内传播,很可能触发其它元素的事件;不需要委托的一般情况下,则宜将内部元素直接绑定事件(不再加selector),使用event.stopPropagation(),就可以阻止冒泡从这个元素向上传。
5. 如果我们只是希望点击目标元素才触发事件,而不希望目标元素的后代元素向上冒泡导致事件被触发,那么方法为:主动增加一个停止冒泡的事件绑定,
方法1:绑定后代元素并在Handler函数中加入event.stopPropagation()
方法2:绑定后代元素并在Handler函数中加入return false, 或直接设置handler为false:
$(‘div *’).on(‘click’, false);
方法3:
$('ul').on('click', 'li', function () {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() === 'li'){
// do something;
}
});
例如搜索框,我们需要点击搜索框和智能选项下拉列表之外的区域时,列表消失,点击搜索框和列表本身不会消失,那么我们要给外层body绑定点击事件,但是要阻止内部的form点击冒泡到body。于是我们主动给form绑定一个事件,事件函数用function ( ) {return false;},简写为false;或function () { event.stopPropagation();}
/*单纯阻止后代元素的冒泡*/
$('form').on('click', function () {
//return false或;
event.stopPropagation();
});
/*后代元素冒泡被阻止了,可以放心绑定事件了*/
$(document).on('click', function () {
$('.smartUl').hide();
});
总的说来,搞清事件冒泡起始、终止节点、传播路径、停止冒泡方式、事件队列、触发方式,和它与.on()方法、[.selector]的关系,就基本能在实践中对事件的处理游刃有余。