在我之前关于DOM的文章里,其实已经有提到过事件的概念。在讲事件之前,首先要知道的就是javascript是由事件驱动的。什么叫事件驱动呢?打个比方,比如我们在页面中点击一个按钮,才会跳出一个窗口或者一句话,又或者说,我们鼠标滚轮移动的时候,出现了一些动画之类的,这就是事件驱动。那什么是事件呢?事件本质上来说就是个触发和响应的过程。事件有三部分组成:事件源,事件名和事件处理函数。所谓事件源,就是触发事件的对象,事件名则是触发了什么事件,事件处理函数则是事件触发后索要执行的函数。比如下述代码
btn.οnclick= function () { alert("啦啦啦"); }
在这里,事件源是btn,事件名称click,事件处理函数是onclick也可以说是后面那个function匿名函数,他们两个是等价的。在这里,我们用的是on加事件名的方式来注册事件的。除了这种方式之外,我们还有另外的注册时间的方式。就是addEventListener()这个方法有三个参数。第一个参数是事件名称,第二个参数是事件监听者,也就是事件处理函数。第三个参数则是一个boolean类型的值,不过他涉及到了事件捕获的概念,这里就先不多提了,一般情况下,我们默认这个值为false。具体使用方法可以看下述代码
btn.addEventListener("click", function () { alert("啦啦啦"); }, false);
这段代码跟上述代码执行的效果是一样的。可能会有人觉得,上面那种方法这么简单,为什么还要有下面的呢?一开始我也是这么觉得的,下面的繁琐又难记,而且觉得也没什么用,所一直用的是前一种方法。不过,都是在做小项目的时候,直到后来,如果给同一个事件源添加了两个相同事件的时候,前一个就会被覆盖掉。也就是说,用on的方式只能注册一个相同的事件。而addEventListener则可以给对象注册多个同名事件,不会造成覆盖。不过,这个方法也有他的局限性,那就是低版本浏览器不支持...在ie8及以下的版本中,他只支持attachEvent()这个方法,这个方法有两个参数,第一个参数是事件名称,第二个参数是事件监听者。这种方法在在新版本浏览器中已经不支持了,包括ie11。但是没办法ie8到现在为止还是有很大的市场份额,所以作为程序猿的我们只能是封装兼容性代码了。
var EventTools = { addEventListener : function (element, type, listener) { if(element.addEventListener) { element.addEventListener(type, listener, false); }else if(element.attachEvent) { element.attachEvent("on" + type, listener); }else{ element["on" + type] = listener; } } };
这里,我把兼容性代码放在了一个EventTools对象中,这样做的好处就是减少变量污染。之前的文章中我有提到过变量污染,不过没有细讲。所谓变量污染,对于平时练习的小项目中,这个问题是很少的,比如我在全局作用域中定义了两次相同名字的变量。这时候,后面定义的变量就会把前面的变量覆盖掉,如果是小项目中,这个问题出现只要改其中一个变量名就好了,可是在大的开发中,如果遇到这样的问题,要找出问题的错误之处是很麻烦的。所以说,我们要尽量去减少变量污染,比如上述方法,我们把有相似一类功能的方法封装在一个对象中,方法名是对象的一个属性,并不具有全局性,这时候就减少了变量的污染。
好了,说完了变量的污染,就该来说说这段代码了,这段代码首先先判断了addEventListener这个方法浏览器是否支持。如果支持,则给元素添加事件监听,否则,判断是否支持attachEvent这个方法,如果支持,则用这种方法给元素添加事件监听。最后一种方法则是为了兼容一些很老版本的浏览器了,就是浏览器这两种方法都不支持的时候,就用on的方式给元素添加事件,不过现在在用的浏览其中基本上都是支持前两种方法的。所以最后一种情况不用怎么考虑。
说完了两种给元素添加事件的方法,就该来说说事件的参数了,在事件触发的时候,系统会自动给事件处理函数传入一个参数,这个我们可以通过这个参数来获取一些事件相关的数据。如果我们不需要这些数据的话,这个参数可以省略。不过现在我们考虑的是有参的情况。
document.onmousemove = function (e) { console.log(e.clientX + "===" + e.clientY); }
上述代码中,我们给document注册了一个onmousemove的事件,当鼠标在页面中移动的时候,打印出当前鼠标距离document左侧和顶部的距离。随着鼠标在页面中不断地移动,我们可以在控制台中看到他不断地在输出鼠标距离浏览器顶部和底部的距离。这里的事件参数就是e,这个参数也可以用其他名字,不过通常情况下,我们都会使用e这个参数。不过这种传参的方式也有他的局限性,就是ie8及以下的版本不支持。不过他的兼容性代码比较简单,我们只需要使用短路运算就能搞定了
e = e || window.event;
解决了兼容性问题,我们就可以用这种方法做很多是事了,比如我们鼠标移动时,把他的e.clientX值和e.clientY值赋给一图片,这样,我们就能实现图片跟随鼠标移动的效果了,比如下述代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> #i1 { position: absolute; } </style> <script> window.onload = function () { var i1 = document.getElementById("i1"); document.onmousemove = function (e) { i1.style.left = e.clientX - 20 + "px"; i1.style.top = e.clientY -20 + "px"; } } box.onclick= function () { alert("啦啦啦"); } </script> </head> <body> <img id="i1" src="images/tianshi.gif" alt=""> </body> </html>
在这个页面中,我们就能实现图片跟随鼠标移动,不过他还是会有一个小问题,就是如果我们的页面出现了滚动条的话,图片跟鼠标就会错位。这时候,我们就要用到了另一种方法就是,就是用pageX和pageY来代替e.clientX和e.clientY。pageX和pageY获取的是相对于浏览器的距离。就不会出现之前的情况了,不过,好的东西总是有他的局限性...ie8还是不支持这个属性。这时候,我们只能再次封装兼容性代码了。
function getPage(e) { return { pageX: e.pageX || (e.clientX + scroll().scrollLeft), pageY: e.pageY || (e.clientY + scroll().scrollTop) } } function scroll() { return { scrollLeft:document.body.scrollLeft || document.documentElement.scrollLeft, scrollTop:document.body.scrollTop || document.documentElement.scrollTop }; }
上述代码中,我封装了两个兼容性代码,我们一个个来分析,首先先判断浏览器是否支持pageX和pageY这两个方法,如果支持,则返回e.pageX或e.pageY否则就使用e.clientX和e.clientY加上被卷去的距离。这时候,又涉及到了另一个兼容性问题,就是相卷的距离的判断,有些浏览器是根据body来判断的,而有些浏览器则是 根据html来判断的。这时候,我们就又要来封装兼容性代码了,如果是根据body来判断,就返回相对于body被卷去的距离,如果是相对于html来判断,那么前一个值是undefined这时候就返回后面的值。这样,浏览器的兼容问题就解决了。
上面说了很多关于clientX和clientY,其实,跟他一个系列的还有两个属性,就是clientWidth和clientHeight。他获取的也是对象内容加padding的值,如果有滚动条的话,这个值也包括滚动条。我们可以用它来获取页面可视区域的大小,不过我们用的多是innerWidth和innerHeight的方式。只不过,他们也是有兼容性问题的。所以,我们只能再封装兼容性代码。
function client() { return { clientWidth: window.innerWidth || document.body.clientWidth || document.documentElement.clientWidth || 0, clientHeight : window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight || 0 }; }
如果浏览器支持用inner的方式获取宽高,就是用inner的方式,否则就是后面两种,后面两种,分别判断的时候如果浏览器是根据body来获取宽度的,或者是html来获取宽度的。如果三种都不支持的话,则返回0。
到这里,BOM中的三大家族已经都说完了。分别是offset系列,scroll系列和client系列。可能有些人有些搞混,不过下面三张图片可以帮我们更容易的区别这三者。
要注意区分的是,client和offset系列的话是相对于offsetParent而言的,也就是往上数第一个有定位的父元素。而client相对于offsetParent的值用的是X和Y。