JavaScript 事件对内存和性能的影响\2016前端面试题之手写事件模型及事件代理/委托
http://www.codeceo.com/article/javascript-event-optimization.html
虽说事件处理程序可以为现代 Web 页面添加很强的交互能力,但是不分青红皂白就添加大量的事件处理程序绝对是一种愚蠢的行为。
我们来分析一下:事件处理程序本质上是一种函数,是一种对象,存放在内存中,设置大量的事件处理程序会使内存中的对象变多,Web 程序的性能会变得越来越差,用户体验很不好。
为了更好地利用好事件处理程序,便出现了事件委托,用来提升性能。
事件委托
事件委托(event delegation):把若干个子节点上的相同事件的处理函数绑定到它的父节点上去,在父节点上统一处理从子节点冒泡上来的事件,这种技术就叫做事件委托。
补充一下:事件委托并不局限于父节点与子节点之间。也可以这样玩,比如页面文档中有好多个处在不同位置地 button,都是绑定 click 事件,使用事件委托,我们可以把这些个事件统一绑定到 body 元素,然后再进行处理(虽然一般很少这么用)。
下面举例子逐步说明事件委托的优势:
<ul id="parent-list">
<li id="list-1">List 1</li>
<li id="list-2">List 2</li>
<li id="list-3">List 3</li>
<li id="list-4">List 4</li>
<li id="list-5">List 5</li> </ul>
假设有上面的代码,我们现在有一个需求:就是无论单击上面的列表(ul)的哪个子列表(li),都会弹出一个框,来显示我们点击了哪个子列表。
需求不难吧?有了需求,接下来是该写 js 代码了,现在有两种方法放在你眼前:1. 为每个 li 子元素绑定 click 事件,然后设置处理函数; 2. 利用事件委托,为 ul 父元素绑定 click 事件,然后设置处理函数
// 方法一 var list1 = document.getElementById("list-1"); list1.addEventListener("click",function(){ alert(this.firstChild.nodeValue); },false); var list2 = document.getElementById("list-2"); list2.addEventListener("click", function() { alert(this.firstChild.nodeValue); }, false); var list3 = document.getElementById("list-3"); list3.addEventListener("click", function() { alert(this.firstChild.nodeValue); }, false); var list4 = document.getElementById("list-4"); list4.addEventListener("click", function() { alert(this.firstChild.nodeValue); }, false); var list5 = document.getElementById("list-5"); list5.addEventListener("click", function() { alert(this.firstChild.nodeValue); }, false);
// 方法二 var parentList = document.getElementById("parent-list"); parentList.addEventListener("click",function(){ var target = event.target; if(target.nodeName.toLowerCase() === "li"){ alert(target.firstChild.nodeValue); } },false);
看着上面的代码,我这里写几点方法二的优点:1. 减少了访问 DOM 的次数,提升了性能;2. 将子元素的事件处理程序统一绑定到其父元素,减少了对内存的占用;3. 可以更好地管理事件处理程序,比如移除对某个事件处理程序的引用
注意:如果对各个子元素的需求不一样,我们还可以这样来改写上面的方法二:
// 方法二 var parentList = document.getElementById("parent-list"); parentList.addEventListener("click",function(){ var target = event.target; if(target.nodeName.toLowerCase() === "li"){ switch(target.id){ case "list-1": alert("学的越多,越觉得自己无知!"); break; case "list-2": alert("爱是一种艺术!"); break; case "list-3": target.innerHTML = "呵呵,我改了啊!"; break; case "list-4": target.style.background = "#aaa"; break; case "list-5": target.style.color = "red"; target.style.fontSize = "2em"; break; default: break; } } },false);
因为事件委托依赖事件冒泡机制,所以,并不是所有的事件都可以进行事件委托。
最适合采用事件委托的事件包括:click、mousedown、mouseup、keydown、keyup 和 keypress。
事件委托只是一种非常不错的事件绑定的思想,所以不应该拘泥于上面的例子,要活学活用! ^_^
移除事件处理程序
我们前面说过,事件处理程序存在于内存中,每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行就越慢。前面所说的事件委托就是用来限制建立的连接数量。
还有,就是内存中那些使用完后不再使用的事件处理程序,如果不释放掉,也会影响 Web 应用程序的内存和性能。
<button id="button">提交</button> var button = document.getElementById("button"); button.onclick = function(){ // 提交某个表单的操作代码 button.onclick = null; // 移除事件处理程序 event.target.firstChild.nodeValue = "提交中。。。"; };
总的原则就是:移除掉那些过时不再使用的事件处理程序,释放内存!
参考资料
- 【书】《JavaScript 高级程序设计(第三版)》
- 【文章】2016前端面试题之手写事件模型及事件代理/委托(推荐)
http://www.w3cmark.com/2016/439.html
在前端面试,js是重头戏,也是体现面试者的重要方面。jq库类在前端影响深远,以至于很多入门者直接用jq代替原生js来开发项目,效率是提升了,但是往往面试官为了考察面试者的基础,几乎不可能问你jq里面的某个功能怎么用,而是问你怎么用原生js去实现某个方法或者考察你是否读个jq的源码,是否懂得里面真正的原理。
Q:描述下js里面的事件流
A:DOM2级事件模型中规定了事件流的三个阶段:捕获阶段、目标阶段、冒泡阶段,低版本IE(IE8及以下版本)不支持捕获阶段
捕获事件流:Netscape提出的事件流,即事件由页面元素接收,逐级向下,传播到最具体的元素。
冒泡事件流:IE提出的事件流,即事件由最具体的元素接收,逐级向上,传播到页面。
关于js事件,这里有一篇非常详细的介绍,可以看下:http://www.cnblogs.com/hyaaon/p/4630128.html
Q:IE和W3C不同绑定事件解绑事件的方法有什么区别,参数分别是什么,以及事件对象e有什么区别
A:
绑定事件:
W3C:target.addEventListener(event, listener, useCapture);
event —— 事件类型;listener —— 事件触发时执行的函数;useCapture —— 指定事件是否在捕获或冒泡阶段执行,为true时事件句柄在捕获阶段执行,为false(默认false)时,事件句柄在冒泡阶段执行。
对应的事件移除:removeEventListener(event,
function
,capture/bubble);
IE:target.attachEvent(type, listener);
type - 字符串,事件名称,含“on”,比如“onclick”、“onmouseover”、“onkeydown”等。 listener —— 实现了 EventListener 接口或者是 JavaScript 中的函数。
对应的事件移除:detachEvent(event,
function
);
Q:事件的委托(代理 Delegated Events)的原理以及优缺点
A:委托(代理)事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。这是靠事件的冒泡机制来实现的,
优点是:
(1)可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒
(2)可以实现当新增子对象时无需再次对其绑定事件,对于动态内容部分尤为合适
缺点是:
事件代理的应用常用应该仅限于上述需求下,如果把所有事件都用代理就可能会出现事件误判,即本不应用触发事件的被绑上了事件。
var toolbar = document.querySelector(".toolbar"); toolbar.addEventListener("click", function(e) { var button = e.target; if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); });
单击button元素会冒泡到UL.toolbar元素,使用了e.target来定位到当前点击的button。
Q:手写原生js实现事件代理,并要求兼容浏览器
A:其实就是考核对事件对象e的了解程度,以及在IE下对应的属性名。
<script type="text/javascript"> // ============ 简单的事件委托 function delegateEvent(interfaceEle, selector, type, fn) { if(interfaceEle.addEventListener){ interfaceEle.addEventListener(type, eventfn); }else{ interfaceEle.attachEvent("on"+type, eventfn); } function eventfn(e){ var e = e || window.event; var target = e.target || e.srcElement; if (matchSelector(target, selector)) { if(fn) { fn.call(target, e); } } } } /** * only support #id, tagName, .className * and it's simple single, no combination */ function matchSelector(ele, selector) { // if use id if (selector.charAt(0) === '#') { return ele.id === selector.slice(1); } // if use class if (selector.charAt(0) === '.') { return (' ' + ele.className + ' ').indexOf(' ' + selector.slice(1) + ' ') != -1; } // if use tagName return ele.tagName.toLowerCase() === selector.toLowerCase(); } //调用 var odiv = document.getElementById('oDiv'); delegateEvent(odiv,'a','click',function(){ alert('1'); }) </script>
Q:实现事件模型
A:大致实现思路就是创建一个类或是匿名函数,在bind和trigger函数外层作用域创建一个字典对象,用于存储注册的事件及响应函数列表,bind时,如果字典没有则创建一个,key是事件名称,value是数组,里面放着当前注册的响应函数,如果字段中有,那么就直接push到数组即可。trigger时调出来依次触发事件响应函数即可。
Q:事件如何派发也就是事件广播(dispatchEvent)
A:一般我们在元素上绑定事件后,是靠用户在这些元素上的鼠标行为来捕获或者触发事件的,或者自带的浏览器行为事件,比如click,mouseover,load等等,有些时候我们需要自定义事件或者在特定的情况下需要触发这些事件。这个时候我们可以使用IE下fireEvent方法,高级浏览器(chrome,firefox等)有dispatchEvent方法。
补充:
(1)冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。
IE 5.5: div -> body -> document
IE 6.0: div -> body -> html -> document
Mozilla 1.0: div -> body -> html -> document -> window
(2)捕获型事件(event capturing):事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。
(3)DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件,但是,捕获型事件先发生。两种事件流会触及DOM中的所有对象,从document对象开始,也在document对象结束。
DOM事件模型最独特的性质是,文本节点也触发事件(在IE中不会)。
支持W3C标准的浏览器在添加事件时用addEventListener(event,fn,useCapture)方法,基中第3个参数useCapture是一个Boolean值,用来设置事件是在事件捕获时执行,还是事件冒泡时执行。而不兼容W3C的浏览器(IE)用attachEvent()方法,此方法没有相关设置,不过IE的事件模型默认是在事件冒泡时执行的,也就是在useCapture等于false的时候执行,所以把在处理事件时把useCapture设置为false是比较安全,也实现兼容浏览器的效果。
(1)冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。
IE 5.5: div -> body -> document
IE 6.0: div -> body -> html -> document
Mozilla 1.0: div -> body -> html -> document -> window
(2)捕获型事件(event capturing):事件从最不精确的对象(document 对象)开始触发,然后到最精确(也可以在窗口级别捕获事件,不过必须由开发人员特别指定)。
(3)DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件,但是,捕获型事件先发生。两种事件流会触及DOM中的所有对象,从document对象开始,也在document对象结束。
DOM事件模型最独特的性质是,文本节点也触发事件(在IE中不会)。
支持W3C标准的浏览器在添加事件时用addEventListener(event,fn,useCapture)方法,基中第3个参数useCapture是一个Boolean值,用来设置事件是在事件捕获时执行,还是事件冒泡时执行。而不兼容W3C的浏览器(IE)用attachEvent()方法,此方法没有相关设置,不过IE的事件模型默认是在事件冒泡时执行的,也就是在useCapture等于false的时候执行,所以把在处理事件时把useCapture设置为false是比较安全,也实现兼容浏览器的效果。
假设一个元素div,它有一个下级元素p。
<div>
<p>元素</p>
< /div>
这两个元素都绑定了click事件,如果用户点击了p,它在div和p上都触发了click事件,那这两个事件处理程序哪个先执行呢?事件顺序是什么?
两种模型
以前,Netscape和Microsoft是不同的实现方式。
Netscape中,div先触发,这就叫做事件捕获。
Microsoft中,p先触发,这就叫做事件冒泡。
两种事件处理顺序刚好相反。IE只支持事件冒泡,Mozilla, Opera 7 和 Konqueror两种都支持,旧版本的Opera's 和 iCab两种都不支持 。
事件捕获
当你使用事件捕获时,父级元素先触发,子级元素后触发,即div先触发,p后触发。
事件冒泡
当你使用事件冒泡时,子级元素先触发,父级元素后触发,即p先触发,div后触发。
W3C模型
W3C模型是将两者进行中和,在W3C模型中,任何事件发生时,先从顶层开始进行事件捕获,直到事件触发到达了事件源元素。然后,再从事件源往上进行事件冒泡,直到到达document。
程序员可以自己选择绑定事件时采用事件捕获还是事件冒泡,方法就是绑定事件时通过addEventListener函数,它有三个参数,第三个参数若是true,则表示采用事件捕获,若是false,则表示采用事件冒泡。
ele.addEventListener('click',doSomething2,true)
true=捕获
false=冒泡
传统绑定事件方式
在一个支持W3C DOM的浏览器中,像这样一般的绑定事件方式,是采用的事件冒泡方式。
ele.onclick = doSomething2
IE浏览器
如上面所说,IE只支持事件冒泡,不支持事件捕获,它也不支持addEventListener函数,不会用第三个参数来表示是冒泡还是捕获,它提供了另一个函数attachEvent。
ele.attachEvent("onclick", doSomething2);
附:事件冒泡(的过程):事件从发生的目标(event.srcElement||event.target)开始,沿着文档逐层向上冒泡,到document为止。
事件的传播是可以阻止的:
• 在W3c中,使用stopPropagation()方法
• 在IE下设置cancelBubble = true;
在捕获的过程中stopPropagation();后,后面的冒泡过程也不会发生了~
3.阻止事件的默认行为,例如click <a>后的跳转~
• 在W3c中,使用preventDefault()方法;
• 在IE下设置window.event.returnValue = false;
4.不是所有的事件都能冒泡,例如:blur、focus、load、unload