JavaScript DOM 事件模型
javascript dom 事件模型
javascript 是基于面向对象和事件驱动的一门语言,事件模型是 dom 中至关重要的内容,理解事件驱动机制、事件反馈、事件冒泡、事件捕获以及事件委托能帮助我们更好的处理事件,写出更优的代码
事件驱动机制
- 当事件发生时,我们收到事件的反馈,在 javascript 中,事件反馈是我们自行定义的事件处理函数
- 事件,如点击事件、鼠标移入事件等,是每一个元素与生俱来的能力
- 通常说的绑定事件,实际上是绑定事件的反馈,即事件处理函数
- 例如点击一个按钮,按钮元素对象是事件发送器或事件源,事件是鼠标点击事件,事件处理函数是侦听器
- 元素对象发出事件,事件处理函数做出反应,这就是 js 的事件驱动机制
在观察者模式中,事件发送器就是主题,事件处理函数即侦听器就是观察者
绑定事件反馈
-
内联属性
<button onclick="test()">按钮</button>
介于结构和逻辑要相分离,不建议使用内联方式绑定
-
事件句柄
var obtn = document.getelementsbytagname('button')[0]; obtn.onclick = function() { // this -> obtn }
兼容性好,但是重复绑定会覆盖
-
事件监听器
var obtn = document.getelementsbytagname('button')[0]; obtn.addeventlistener("click", funtion(){ // this -> obtn }, false); obtn.addeventlistener("click", test, false); funtion test(){ // 事件处理函数 }
重复添加,不会覆盖之前添加的监听器,但是如果事件类型、事件处理函数和最后一个布尔参数都相同,则不会重复执行
ie8 及以下不支持 addeventlistener,可用 attachevent 代替
var obtn = document.getelementsbytagname('button')[0]; obtn.attachevent("onclick", funtion(){ // this -> window }); // 区别于 addeventlistener,第一个参数使用 'onclick',而不是 'click' // 并且内部 this 指向 window // 对于 attachevent,如果事件类型、事件处理函数都相同,还是会重复执行
兼容性封装
function addevent(elem, type, fn) { if (elem.addeventlistener) { elem.addeventlistener(type, fn, false); } else if (elem.attachevent) { elem.attachevent('on' + type, function(ev) { fn.call(elem, ev); // call 兼容性比 bind 好 }); } else { elem['on' + type] = fn; } }
-
解除绑定
obtn.onclik = null; obtn.removeeventlistener("click", test, false); // 解除 addeventlistener obtn.detachevent('onclick', test); // 解除 attachevent
示例:点击一次后清除事件反馈
obtn.onclik = function() { // ... this.onclick = null; } // 非严格模式 obtn.addeventlistener("click", funtion() { // ... this.removeventlistener('cilck', arguments.callee, false); }, false); // 严格模式 obtn.addeventlistener("click", funtion temp() { // ... this.removeeventlistener('click', temp, false); }, false);
事件冒泡和捕获
-
事件冒泡:当一个元素发生事件时,该事件会向父级元素传递,按由子到父的顺序触发一连串的事件反馈,称之为事件冒泡
dom 上的嵌套关系会产生事件冒泡,例如两个 div 嵌套,点击内部的 div,触发内部 div 的点击事件,内部 div 的点击事件处理函数进行响应,这个事件向其父级即外部 div 传递,外部 div 也有点击事件,外部 div 所绑定的点击事件反馈也会响应
<div class="outer"> <div class="inner"></div> </div>
var outer = document.getelementsbyclassname('outer')[0], inner = outer.getelementsbyclassname('inner')[0]; outer.addeventlistener('click', function () { console.log('bubble outer'); }, false); inner.addeventlistener('click', function () { console.log('bubble inner'); }, false); // addeventlistener 最后一个参数默认值为 false,表示事件冒泡 // 点击 inner,打印出 // bubble inner // bubble outer
-
事件捕获:当一个元素发生事件时,该事件会向父级元素传递,按由父到子的顺序触发一连串的事件反馈,称之为事件捕获
事件捕获与事件冒泡的触发顺序相反,同样需要 dom 上的嵌套关系
outer.addeventlistener('click', function () { console.log('outer'); }, true); inner.addeventlistener('click', function () { console.log('inner'); }, true); // addeventlistener 最后一个参数使用 true,表示事件捕获 // 点击 inner,打印出 // outer // in
-
捕获和冒泡的执行顺序
outer.addeventlistener('click', function () { console.log('bubble outer'); }, false); // 冒泡 inner.addeventlistener('click', function () { console.log('bubble inner'); }, false); // 冒泡 outer.addeventlistener('click', function () { console.log('outer'); }, true); // 捕获 inner.addeventlistener('click', function () { console.log('inner'); }, true); // 捕获 // 点击 inner,打印出 // outer // bubble inner // inner // bubble outer
点击一个元素,元素即事件源,若事件源绑定了事件处理函数,且设定了事件捕获,则先执行捕获,捕获执行完毕后,按照绑定顺序执行该事件源绑定的事件,如果设定了事件冒泡,再执行冒泡
-
focus blur change submit reset select 事件没有冒泡和捕获,ie 浏览器没有事件捕获
阻止事件冒泡
-
阻止冒泡的方法
event 的原型上有 stoppropagation 方法,可以阻止冒泡,是 w3c 的规范
event 的原型上有 canclebubble 属性,赋值为 true,可以阻止冒泡
-
addeventlistener 绑定事件处理函数,拿到事件对象
var outer = document.getelementsbyclassname('outer')[0], inner = outer.getelementsbyclassname('inner')[0]; inner.addeventlistener('click', function (ev) { console.log(ev); // 事件对象 ev ev.stoppropagation(); // 阻止事件冒泡 }, false);
-
ie 浏览器没有 stoppropagation 方法,可以使用 cancelbubble 属性
注意:ie 浏览器中事件对象存放在 window.event 中。ie8 不支持 addeventlistener 方法
// 封装阻止冒泡的方法 function cancelbubble(ev) { if (ev.stoppropagation) { ev.stoppropagation(); } else ev.cancelbubble = true; // 兼容 ie8 及以下 } // 使用上文中封装好的 addevent 方法 function addevent(elem, type, fn) { if (elem.addeventlistener) { elem.addeventlistener(type, fn); } else if (elem.attachevent) { elem.attachevent('on' + type, function (ev) { fn.call(elem, ev); }); } else { elem['on' + type] = fn; } } // 绑定事件处理函数 var outer = document.getelementsbyclassname('outer')[0], inner = outer.getelementsbyclassname('inner')[0]; addevent(inner, 'click', function (ev) { var ev = ev || window.event; // ie 兼容性写法 cancelbubble(ev); // 阻止冒泡 });
阻止默认事件
-
三种方法
- 事件对象 preventdefault() 方法,兼容 ie9 及以上
- 事件对象 returnvalue = false,兼容 ie8 及以下
- 事件处理函数 return false
-
兼容性写法
function preventdefaultevent(ev) { if (ev.preventdefault) { ev.preventdefault(); } else ev.returnvalue = false; // 兼容 ie8 及以下 }
-
右键菜单事件
document.oncontextmenu = function (ev) { var ev = ev || window.event; // 1. ev.preventdefault(); // ie9 及以上 // 2. ev.returnvalue = false; // ie8 及以下 // 3. return false; }
-
a 标签跳转事件
href 使用伪协议
<a href="javascript:void(0);">a 标签</a> <a href="javascript:;">a 标签</a> <a href="#">a 标签</a> <!--跳转到当前页面顶部-->
onclick 事件 return false
<a href="http://www.baidu.com" onclick="return false">a 标签</a> <a href="http://www.baidu.com" onclick="return test(),false">a 标签</a> <!--第二个是利用了 “,” 分隔符会返回最后一个的特点,与 test 方法无关-->
绑定事件处理函数
<!--内联绑定--> <a id='taga' href="http://www.baidu.com" onclick="return test()">a 标签</a> <!--句柄绑定--> <script> document.getelementbyid('taga').onclick = test; function test(ev) { var ev = ev || window.event; // 1. ev.preventdefault(); // ie9 及以上 // 2. ev.returnvalue = false; // ie8 及以下 // 3. return false; } // 前两种方式在使用内联属性绑定时,不需要在属性上加 return,第三种则需要 </script>
表单的 action 属性支持
javascript:
伪协议,onsubmit 或者提交按钮点击事件都可以绑定处理函数,阻止提交的方法和阻止 a 标签跳转的方法类似
冒泡捕获流
-
事件流:描述从页面中接收事件的顺序
-
事件冒泡流:微软 ie 提出,event bubbling
-
事件捕获流:网景 netscape 提出,event capturing
-
事件流三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
元素触发事件时,首先事件捕获阶段,由父到子的执行事件处理函数,然后处于目标阶段,该元素的事件处理函数按绑定顺序执行,最后事件冒泡阶段,由子到父的执行事件处理函数
事件和事件源
-
事件即事件对象,可以由事件处理函数的参数拿到
ie8 及以下中事件对象存放在 window.event 中
// btn 按钮元素 btn.onclick = function(ev) { var ev = ev || window.event; // ie8 兼容性写法 }
-
事件源即事件源对象,是发生事件的元素,即事件发送器,可以从事件对象中获取
ie8 及以下只有 srcelement,firefox 低版本只有 target,chrome 两者都有
// btn 按钮元素 btn.onclick = function(ev) { var ev = ev || window.event; // ie8 兼容性写法 var tar = ev.target || ev.srcelement; // 获取事件源的兼容性写法 }
事件委托
-
事件委托也叫事件代理,指对父级元素绑定事件处理函数,通过获取事件源来处理子元素
-
示例:点击按钮使列表 ul 增加 li 元素,点击每个 li 元素打印出其中的内容(innerhtml)
如果不使用事件委托,需要循环对每个 li 进行绑定,点击按钮添加新的 li 元素后也要进行绑定,效率低下
使用事件委托,直接对 ul 绑定点击事件处理函数,获取事件对象、事件源对象,再对源对象进行处理
<body> <button>btn</button> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> <script> var obtn = document.getelementsbytagname('button')[0], olist = document.getelementsbytagname('ul')[0], oli = olist.getelementsbytagname('li'); obtn.onclick = function () { var li = document.createelement('li'); li.innertext = oli.length + 1; olist.appendchild(li); } olist.onclick = function (ev) { var ev = ev || window.event, tar = ev.target || ev.srcelement; // tar 即为被点击的 li 元素 console.log(tar.innerhtml); // 返回在所有兄弟元素中的索引,借用数组 indexof 方法 console.log(array.prototype.indexof.call(oli, tar)); } </script> </body>