How to make an efficient web front-end program is a question I unconsciously consider every time I do front-end development. A few years ago, the awesome front-end engineers at Yahoo published a book on improving web front-end performance, which caused a sensation in the entire web development technology community. The mysterious web front-end optimization problem became a common problem on the street, and web front-end optimization became Simple questions that both rookies and experts can answer. When the entire industry knows the shocking secret answer, then the existing optimization technology can no longer produce a qualitative leap in the website you develop. In order to make the website we develop perform better than others To make our website better, we need to think more deeply independently and reserve better skills. The
Eventsystem in Javascript is the first breakthrough point that comes to my mind. Why is it a JavaScript event system? We all know that the web front-end includes three technologies: html, css and javascript. It is very clear how html and css are combined: style, class, id and html tag. There is not much to say about this. Yes, but how does javascript get in between html and css, so that the three can be integrated? Finally, I found that this entry point is the event system of JavaScript. No matter how long or complex the JavaScript code we write, it will eventually be reflected in HTML and CSS through the event system. So I was thinking that since the event system is the entry point for the integration of the three point, then there will inevitably be a large number of event operations in a page, especially in today's increasingly complex web pages. Without these events, our carefully written javascript code will only be stored in the database, and the hero will be useless. Since there will be a large number of eventfunctions on the page, will there be any problems affecting efficiency if we write event functions according to our habits? The answer I have researched is that there is a real efficiency problem, and it is also a serious efficiency problem.
In order to explain my answer clearly, I need to first explain the JavaScript event system in detail.
The event system is the entry point for the integration of javascript, html and css. This entry point is like the main function in java. All the magic starts here. So how does the browser complete it? What about this cut-in? I have researched a total of 3 methods, they are:
Method 1: htmlEvent processing
html Event processing is to write the event function directly in the html tag. Because this writing method is tightly coupled with the html tag, it is called html event processing. For example, the following code:
<input type="button" id="btn" name="btn" onclick="alert('Click Me!')"/>
If the click event function is complicated, writing the code like this will definitely cause inconvenience, so we often write the function externally, and onclick directly calls the function name, for example:
<input type="button" id="btn" name="btn" onclick="btnClk()"/> function btnClk(){ alert("click me!"); }
The above writing method is a very beautiful writing method, so many people still use it unconsciously nowadays, but maybe many people don’t know that the latter writing method is actually not as robust as the former one. This is what I was studying very recently. Problems encountered when blocking loading script technology, because according to the principle of front-end optimization, javascript code is often located at the bottom of the page. When the page is blocked by scripts, the function referenced by in the html tag may not have been executed yet. At this time, when we click the page button , the result will be "XXX function undefined error". In JavaScript, such errors will be captured by try and catch, so in order to make the code more robust, We will have the following rewriting:
<input type="button" id="btn" name="btn" onclick="try{btnClk();}catch(e){}"/>
Method 2: DOM0 level event processing
DOM0 level event processing is an event processing supported by all browsers today, and there is no compatibility Sexual issues, seeing such a sentence will make everyone who works on the web front-end excited. The rules for DOM0 event processing are: Each DOM element has its own event processingattribute , which can be assigned a function, such as the following code:
var btnDOM = document.getElementById("btn"); btnDOM.onclick = function(){ alert("click me!"); }
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函数未定义”的问题了。总结一下这个特点:事件委托代码可以运行在页面加载的任何阶段,这点对提升网页性能还是增强网页效果上都会给开发人员提供更大自由度。
The above is the detailed content of Example code for high-performance writing of JavaScript events. For more information, please follow other related articles on the PHP Chinese website!