어떻게 하면 효율적인 웹 프론트엔드 프로그램을 만들 수 있느냐는 프론트엔드 개발을 할 때마다 무의식적으로 고민하는 질문입니다. 몇 년 전 야후의 뛰어난 프론트엔드 엔지니어들이 웹 프론트엔드 성능 향상에 관한 책을 출판했는데, 이는 전체 웹 개발 기술 커뮤니티에 센세이션을 일으켰습니다. 신비한 웹 프론트엔드 최적화 문제는 웹 상에서 공통적인 문제가 되었습니다. 초보도 전문가도 답할 수 있는 간단한 질문이 됐습니다. 업계 전체가 충격적인 비밀 답을 알게 되면 기존 최적화 기술로는 더 이상 개발하는 웹사이트의 질적 도약을 이룰 수 없습니다. 우리가 개발하는 웹사이트가 다른 웹사이트보다 더 나은 성능을 발휘하도록 만들기 우리의 웹사이트를 더 좋게 만들기 위해서는 더 독립적으로 생각하고 더 나은 기술을 보유해야 합니다.
Javascript의 이벤트 시스템이 가장 먼저 떠오르는 획기적인 포인트입니다. 왜 JavaScript 이벤트 시스템인가요? 우리 모두는 웹 프론트엔드에 html, css, javascript의 세 가지 기술이 포함되어 있다는 것을 알고 있습니다. html과 css가 어떻게 결합되는지는 매우 명확합니다: 스타일, 클래스, id 및 html 태그 이에 대해 할 말이 많지 않습니다. 하지만 javascript는 html과 css 사이에 어떻게 들어가서 이 세 가지를 통합할 수 있나요? 마지막으로 이 진입점이 JavaScript의 이벤트 시스템이라는 것을 알게 되었습니다. 우리가 작성하는 JavaScript 코드가 아무리 길거나 복잡하더라도 결국 이벤트 시스템을 통해 HTML과 CSS에 반영될 것이라고 생각했습니다. 은 세 가지 포인트 통합을 위한 진입점이므로 페이지에 필연적으로 많은 수의 이벤트 작업이 있을 것입니다. 특히 오늘날 점점 더 복잡해지는 웹 페이지에서는 이러한 이벤트가 없으면 신중하게 작성된 자바스크립트 코드가 다음 위치에만 저장됩니다. 데이터베이스와 영웅은 쓸모가 없습니다. 페이지에 수많은 이벤트함수가 있을 예정인데, 습관대로 이벤트 함수를 작성하면 효율성에 영향을 미치는 문제가 있을까요? 제가 조사해 본 답은 정말 효율성 문제이고, 심각한 효율성 문제라는 것입니다.
제 답변을 명확하게 설명하려면 먼저 JavaScript 이벤트 시스템에 대해 자세히 설명해야 합니다.
이벤트 시스템은 javascript, html 및 css 통합을 위한 진입점입니다. 이 진입점은 java의 main 함수와 같습니다. 그러면 브라우저는 어떻게 시작됩니까? 완료해 보세요. 이 컷인은 어떻습니까? 총 3가지 방법을 조사했는데,
방법 1: html이벤트 처리
html 이벤트 처리는 html 태그에 이벤트 함수를 직접 작성하는 것입니다. 이 작성 방법은 html 태그와 긴밀하게 결합되어 있으므로 html 이벤트 처리라고 합니다. 예를 들어 다음과 같은 코드는
<input type="button" id="btn" name="btn" onclick="alert('Click Me!')"/>
클릭 이벤트 함수가 복잡하다면 이렇게 코드를 작성하면 당연히 불편함이 따르기 때문에 함수를 외부에서 작성하고 onclick이 직접 함수명을 호출하는 경우가 많습니다. :
<input type="button" id="btn" name="btn" onclick="btnClk()"/> function btnClk(){ alert("click me!"); }
위의 글쓰기 방식은 매우 아름다운 글쓰기 방식이라 요즘에도 무의식적으로 사용하는 분들이 많은데, 실제로는 후자의 글쓰기 방식이 전자만큼 강렬하지 않다는 사실을 모르는 분들이 많을 수도 있겠네요 최근에 제가 공부한 내용인데, 스크립트 로딩 기술을 차단할 때 발생하는 문제는 프론트엔드 최적화 원칙에 따라 자바스크립트 코드가 스크립트에 의해 페이지 하단에 위치하는 경우가 많기 때문입니다. html 태그에서 이 참조하는 함수가 아직 실행되지 않았을 수 있습니다. 이때 페이지 버튼 을 클릭하면 "XXX 함수 정의되지 않은 오류"가 발생합니다. try and catch로 캡처되므로 코드를 더욱 강력하게 만들기 위해 다음과 같이 다시 작성합니다.
<input type="button" id="btn" name="btn" onclick="try{btnClk();}catch(e){}"/>
방법 2: DOM0레벨 이벤트 처리
DOM0 레벨 이벤트 처리는 현재 모든 브라우저에서 지원하는 이벤트 처리로, 호환성 없음 성적인 문제, 이런 문장을 보면 웹 프론트 엔드에서 일하는 모든 사람들이 흥분하게 될 것입니다. DOM0 이벤트 처리 규칙은 다음과 같습니다. 각 DOM 요소에는 다음 코드와 같이 함수를 할당할 수 있는 고유한 이벤트 처리속성 이 있습니다.
var btnDOM = document.getElementById("btn"); btnDOM.onclick = function(){ alert("click me!"); }
자바스크립트 객체라는 것을 알고 있으므로 자바스크립트 객체의 관점에서 DOM 레벨 0 이벤트 처리를 이해하는 것은 매우 쉽습니다. 예를 들어 다음 코드는 입니다.
btnDOM.onclick = null;
btnDOM.onclick = function(){ alert("click me!"); } btnDOM.onclick = function(){ alert("click me1111!"); }
后面一个函数会将第一个函数覆盖。
方式三:DOM2事件处理和IE事件处理
DOM2事件处理是标准化的事件处理方案,但是IE浏览器自己搞了一套,功能和DOM2事件处理相似,但是代码写起来就不太一样了。
在讲解方式三之前,我必须要补充一些概念,否则是无法讲清楚方式三的内涵。
第一个概念是:事件流
在页面开发里我们常常会碰到这样的情况,一个页面的工作区间在javascript可以用document表示,页面里有个p,p等于是覆盖在document元素上,p里面有个button元素,button元素是覆盖在p上,也等于覆盖着document上,所以问题来了,当我们点击这个按钮时候,这个点击行为其实不仅仅发生在button之上,p和document都被作用了点击操作,按逻辑这三个元素都是可以促发点击事件的,而事件流正是描述上述场景的概念,事件流的意思是:从页面接收事件的顺序。
第二个概念:事件冒泡和事件捕获
事件冒泡是微软公司提出解决事件流问题的方案,而事件捕获则是网景公司提出的事件流解决方案,它们的原理如下图:
冒泡事件由p开始,其次是body,最后是document,事件捕获则是倒过来的先是document,其次是body,最后是目标元素p,相比之下,微软公司的方案更加人性化符合人们的操作习惯,网景的方案就很别扭了,这是浏览器大战的恶果,网景慢了一步就以牺牲用户习惯的代码解决事件流的问题。
微软公司结合冒泡事件设计了一套新的事件系统,业界习惯称为ie事件处理,ie事件处理方式如下面代码所示:
var btnDOM = document.getElementById("btn"); btnDOM.attachEvent("onclick",function(){ alert("Click Me!"); });
在ie下通过DOM元素的attachEvent方法添加事件,和DOM0事件处理相比,添加事件的方式由属性变成了方法,所以我们添加事件就需要往方法里传递参数,attachEvent方法接收两个参数,第一个参数是事件类型,事件类型的命名和DOM0事件处理里的事件命名一样,第二个参数是事件函数了,使用方法的好处就是如果我们在为同一个元素添加个点击事件,如下所示:
btnDOM.attachEvent("onclick",function(){ alert("Click Me!"); }); btnDOM.attachEvent("onclick",function(){ alert("Click Me,too!"); });
运行之,两个对话框都能正常弹出来,方法让我们可以为DOM元素添加多个不同的点击事件。如果我们不要某个事件呢?我们该怎么做了,ie为删除事件提供了detachEvent方法,参数列表和attachEvent一样,如果我们要删除某个点击事件,只要传递和添加事件一样的参数即可,如下代码所示:
btnDOM.detachEvent("onclick",function(){ alert("Click Me,too!"); });
运行之,后果很严重,我们很迷惑,第二个click居然没有被删除,这是怎么回事?前面我讲到删除事件要传入和添加事件一样的参数,但是在javascript的匿名函数里,两个匿名函数哪怕代码完全一样,javascript都会在内部使用不同变量存储,结果就是我们看到的现象无法删除点击事件的,因此我们的代码要这么写:
var ftn = function(){ alert("Click Me,too!"); }; btnDOM.attachEvent("onclick",ftn); btnDOM.detachEvent("onclick",ftn);
这样添加的方法和删除的方法就是指向了同一个对象,所以事件删除成功了。这里的场景告诉我们写事件要有个良好的习惯即操作函数要独立定义,不要用匿名函数用成了习惯。
接下来就是DOM2事件处理,它的原理如下图所示:
DOM2是标准化的事件,使用DOM2事件,事件传递首先从捕获方式开始即从document开始,再到body,p是一个中介点,事件到了中介点时候事件就处于目标阶段,事件进入目标阶段后事件就开始冒泡处理方式,最后事件在document上结束。(捕获事件的起点以及冒泡事件的终点,我本文都是指向document,实际情况是有些浏览器会从window开始捕获,window结束冒泡,不过我觉得开发时候不管浏览器本身怎么设定,我们关注document更具开发意义,所以我这里一律都是使用document)。人们习惯把目标阶段归为冒泡的一部分,这主要是因为开发里冒泡事件使用的更加广泛。
DOM2事件处理很折腾,每次事件促发时候都会把所有元素遍历两遍,这点和ie事件相比性能就差多了,ie只有冒泡,所以ie只需要遍历一次,不过遍历少了并不代表ie的事件体系效率更高,从开发设计角度同时支持两种事件系统会给我们开发带来更大的灵活度,从这个角度而言DOM2事件还是很有可取之处。DOM2事件的代码如下:
var btnDOM = document.getElementById("btn"); btnDOM.addEventListener("click",function(){ alert("Click Me!"); },false); var ftn = function(){ alert("Click Me,too!"); }; btnDOM.addEventListener("click",ftn,false);
DOM2事件处理里添加事件使用的是addEventListener,它接收三个参数比ie事件处理多一个,前两个的意思和ie事件处理方法的两个参数一样,唯一的区别就是第一个参数里要去掉on这个前缀,第三个参数是个布尔值,如果它的取值是true,那么事件就按照捕获方式处理,取值为false,事件就是按照冒泡处理,有第三个参数我们可以理解为什么DOM2事件处理里要把事件元素跑个两遍,目的就是为了兼容两种事件模型,不过这里要请注意下,不管我们选择是捕获还是冒泡,两遍遍历是永远进行,如果我们选择一种事件处理方式,那么另外一个事件处理流程里就不会促发任何事件处理函数,这和汽车挂空挡空转的道理一样。通过DOM2事件方法的设计,我们知道DOM2事件在运行时候只能执行两种事件处理方式中的一种,不可能两个事件流体系同时促发,所以虽然元素遍历两遍,但是事件函数绝不可能被促发两遍,注意我这里指不促发两遍是指一个事件函数,其实我们可以模拟两个事件流模型同时执行的情况,例如下面代码:
btnDOM.addEventListener("click",ftn,true); btnDOM.addEventListener("click",ftn,false);
但这种写法是多事件处理,相当于我们点击两次按钮。
DOM2也提供了删除事件的函数,这个函数就是removeEventListener,写法如下:
btnDOM.removeEventListener("click",ftn,false);
使用和ie事件的一样即参数要和定义事件的参数一致,不过removeEventListener使用时候,第三个参数不传,默认是删除冒泡事件,因为第三个参数不传默认都是false,例如:
btnDOM.addEventListener("click",ftn,true); btnDOM.removeEventListener("click",ftn);
运行之,发现事件没有被删除成功。
最后我要说的是DOM2事件处理在ie9包括ie9以上的版本都得到了很好的支持,ie8以下是不支持DOM2事件的。
下面我们对三种事件方式做个比较,比较如下:
比较一:方式一为一方和其他两种方式比较
方式一的写法是html和javascript结合在一起,你中有我我中有你,把这种方式深化一下就是html和javascript混合开发,用一个软件术语表达就是代码耦合,代码耦合不好,而且是非常不好,这是菜鸟程序员的级别,所以方式一完败,另外两种方式完胜。
比较二:方式二和方式三
它们两个写法差不多,有时真的很难说谁好谁坏,纵观上述内容我们发现方式二和方式三的最大区别就是:使用方式二一个DOM元素某个事件有且只有一次,而方式三则可以让DOM元素某个事件拥有多个事件处理函数,在DOM2事件处理里,方式三还能让我们精确控制事件流的方式,因此方式三的功能比方式二更加的强大,所以相比之下方式三略胜一筹。
下面就是本文的重点:事件系统的性能问题,解决性能问题必须找到一个着力点,这里我从两个着力点来思考事件系统的性能问题,它们分别是:减少遍历次数和内存消耗。
首先是遍历次数,不管是捕获事件流还是冒泡事件流,都会遍历元素,而是都是从最上层的window或document开始的遍历,假如页面DOM元素父子关系很深,那么遍历的元素越多,像DOM2事件处理这种,遍历危害程度就越大了,如何解决这个事件流遍历问题了?我的回答是没有,这里有些朋友也许会有疑问,怎么会没有了?事件系统里有个事件对象即event,这个对象有阻止冒泡或捕获事件的方法,我怎么说没有呢?这位朋友的疑问很有道理,但是如果我们要使用该方法减少遍历,那么我们代码就要处理父子元素的关系,爷孙元素关系,如果页面元素嵌套很多,这就是没法完成的任务,所以我的回答是没法改变遍历的问题,只能去适应它。
看来减少遍历是没法解决事件系统性能问题了,那么现在只有从内存消耗考虑了。我常听人说C#很好用,对于web前端开发它就更好用了,我们可以直接在C#的IDE拖一个按钮到页面,按钮到了页面之后javascript代码会自动为该按钮添加个事件,当然里面的事件函数是个空函数,于是我想我们可以按这种方式在页面放置100个按钮,一个代码都不行就有了100个按钮事件处理,超级方便,最后我们对其中一个按钮添加具体的按钮事件,让页面跑起来,请问大家这个页面效率会高吗?在javascript里,每个函数都是一个对象,每个对象都会耗费内存,所以这无用的99个事件函数代码肯定消耗了很多宝贵的浏览器内存。当然现实开发环境里我们不会这么干的,但是在当今ajax流行,单页面开发疯狂普及的时代,一个网页上的事件都是超级多的,这就意味我们每个事件都有一个事件函数,但是我们每次操作都只会促发一个事件,此时其他事件都是躺着睡觉,起不到任何作用同时还要消耗计算机的内存。
我们需要一种方案改变这种情况,现实中的确有这种方案。为了清晰描述这个方案,我要先补充一些背景知识,在讲述DOM2事件处理里我提到了目标对象这个概念,抛开DOM2事件处理方式,在捕获事件处理和冒泡事件处理里也有目标对象的概念,目标对象就是事件具体操作的DOM元素,例如点击按钮操作里按钮就是目标对象,不管哪个事件处理方式,事件函数都会包含一个event对象,event对象有个属性target,target是永远指向目标对象的,event对象还有个属性就是currentTarget,这个属性指向的是捕获或冒泡事件流动到的DOM元素。由上文描述我们知道,不管是捕获事件还是冒泡事件,事件流都会流动到document上,假如我们在document上添加点击事件,页面上的按钮不添加点击事件,这时候我们点击按钮,我们知道document上的点击事件会促发,这里有个细节就是促发document点击事件时候,event的target的指向是button而不是document,那么我们可以这样写代码:
<input type="button" id="btn" name="btn" value="BUTTON"/> <a href="#" id="aa">aa</a> document.addEventListener("click",function(evt){ var target = evt.target; switch(target.id){ case "btn": alert("button"); break; case "aa": alert("a"); break; } },false);
运行之,我们发现效果和我们单独写按钮事件一样。但是它的好处是不言而喻的,一个函数搞定了整个页面的事件函数,而且没有事件函数被空闲,简直完美,这个方案还有个专业名称:事件委托。jQuery的delegate方法就是按这个原理做的。其实事件委托的效率不仅仅体现在事件函数的减少,它还能减少dom遍历操作,例如上面例子里我们在document上添加函数,document是页面里的顶层对象,读取它的效率是很高的,到了具体的对象事件我们也没有通过dom操作而是使用事件对象的target属性,所有这些只能用一句话概括:真是快,没理由的快。
事件委托还能给我们带来一个很棒副产品,使用过jQuery的朋友都应该用过live方法,live方法特点是你可以为页面元素添加事件操作,哪怕这个元素目前在页面还不存在,你也可以添加它的事件,理解了事件委托机制,live的原理就很好理解了,其实jQuery的live就是通过事件委托做的,同时live还是一种高效的事件添加方式。
理解了事件委托,我们会发现jQuery的bind方法是个低效的方法,因为它使用原始的事件定义方式,所以bind我们要慎用,其实jQuery的开发者也注意到这个问题,新版的jQuery里都有一个on方法,on方法包含了bind、live和delegate方法所有功能,所以我建议看了本文的朋友要摒弃以前使用添加事件的方式,多使用on函数添加事件。
事件委托还有个好处,上文里事件委托的例子我是在document上添加事件,这里我要做个比较,在jQuery里我们习惯把DOM元素事件的定义放在ready方法里,如下所示:
$(document).ready(function(){ XXX.bind("click",function(){}); });
ready函数是在页面DOM文档加载完毕后执行,它比onload函数先执行,这种提前好处很多,好处之一也是带来性能提升,jQuery这种事件定义也算是个标准做法,我相信有些朋友一定又把某些事件绑定放在ready外面,最后发现按钮会无效,这种无效场景有时一刹那,过会儿就好了,所以我们常常忽视了该问题的原理,不在ready函数绑定事件,这个操作其实是在DOM加载完毕之前绑定事件,而这个时间段下,很有可能某些元素还没在页面构造好,所以事件绑定会出现无效情况,因此ready定义事件的道理就是保证页面所有元素加载完毕后在定义DOM元素的事件,但是使用事件委托时可以避免问题的发生,例如将事件绑定在document,document代表整个页面,所以它加载完毕的时间可谓最早,所以在document上实现事件委托,就很难发生事件无效的情况,也很难发生浏览器报出“XXX函数未定义”的问题了。总结一下这个特点:事件委托代码可以运行在页面加载的任何阶段,这点对提升网页性能还是增强网页效果上都会给开发人员提供更大自由度。
위 내용은 JavaScript 이벤트의 고성능 작성을 위한 예제 코드의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!