CSSOM与getOffset函数
本来嘛,既然东西是IE发明的,应该一切都以IE为标准才是,不明白非得让w3c那帮官僚认可才行。后知后觉的w3c唯一做的好事是第一次浏览器大战后,让世界分裂成两大阵营进行冷战。那个时间,苹果被微软打得快破产,为了避免垄断嫌疑,微软资助了苹果,其中一个条件是让Mac也用上IE,不过像safari,opera等小众浏览器,早在第一次浏览器大战中,就实现了许多网景与IE的方法。剩下的就是火狐,以前是没有什么人理睬w3c这个傻冒的,也就火狐诞生后开始大量实现(也可能在暗中制定)许多w3c草案的方法。当然不能说w3c全部都不好,如xpath,xquery等东西,还是很有用,当然也很小众。IE即使没有它们还能活得很好。此后,IE还有一系列与布局有关的方法,如offset家族,scroll家族,client家族与getClientRects() 、getBoundingClientRect()。这些方法非常有用,以致于火狐这样顽固的浏览器最后还是实现了它们。特别是getBoundingClientRect方法,有了它可以速度取得元素节点的坐标与尺寸。最后w3c终于坐不住了,把它们与一些处理样式的方法统一起来,称之为CSSOM(Cascading Style Sheets Object Model,层叠样式表对象模型)。
按草案的提交顺序,分为两部分。第一部分,处理样式表的方法与属性。这方面w3c做得非常失败了,在IE下好像就只有document.styleSheets用得了,不过这方法极可能本来就是IE的。听说IE9全部支持,不过xp不支持IE9也是白搭的!第二部分,处理布局的方法与属性。基本上是IE发明的东西,当然还有一些火狐的东西。现在重点看一下第二部分的内容:
首先是一个叫AbstractView的接口,是所有视图的基础。在FF中我们真的能找到这东西。
alert(window.AbstractView); //[object AbstractView]
然后是Media接口, 我找不到它存在的证明。
再然后是ScreenView接口, 它在FF等标准浏览器叫做document.defaultView。不过实际上它被绑定了比w3c草案更多的属性:
var aa = document.getElementById("aa"); var s = "" for(var name in document.defaultView) if(! /firebug/i.test(name)) s += name+" : "+document.defaultView[name]+"
" aa.innerHTML = s;
alert(window.innerWidth) alert(window.innerHeight) alert(window.outerWidth) alert(window.outerHeight) alert(window.pageXOffset) alert(window.pageYOffset) alert(window.screenX) alert(window.screenY) alert(window.scroll) alert(window.scrollTo) alert(window.scrollBy)
Screen接口,用于提供与用户显示器相关的信息:
alert(screen.availWidth) alert(screen.availHeight) alert(screen.width) alert(screen.height) alert(screen.colorDepth) alert(screen.pixelDepth)
剩下的就不细节了,基本上是IE那一套东西还有对鼠标事件对象的属性进行标准化。它提供了许多计算基准来计算offsetLeft等的值。现在重点看看offsetParent。记得John Resig在《Pro.JavaScript.Techniques》这本严重缺乏检测的书中提供了两个计算元素坐标值的函数来为害人间:
//page 144 // Find the X (Horizontal, Left) position of an element function pageX(elem) { // See if we're at the root element, or not return elem.offsetParent ? // If we can still go up, add the current offset and recurse upwards elem.offsetLeft + pageX( elem.offsetParent ) : // Otherwise, just get the current offset elem.offsetLeft; } // Find the Y (Vertical, Top) position of an element function pageY(elem) { // See if we're at the root element, or not return elem.offsetParent ? // If we can still go up, add the current offset and recurse upw elem.offsetTop + pageY( elem.offsetParent ) : // Otherwise, just get the current offset elem.offsetTop; }
或是整合成如下类似版本:
var getOffseft = function( el ) { var x = 0; var y = 0; do { x += el.offsetLeft; y += el.offsetTop; } while ((el = el.offsetParent)); return {x:x,y:y}; }
但实际来说,IE与标准浏览器早期的版本对这个offsetParent都有些bug,如果要计算偏移量,还要利用offsetLeft与offsetTop,而它们也有bug,因此上面的方法计算的结果与实际出入很大。在jQuery中,John Resig重新设计了这个函数,并且添加一系列辅助方法来获取浏览器相应的特征:
jQuery.fn.offset = function( options ) { var elem = this[0]; if ( options ) { return this.each(function( i ) { jQuery.offset.setOffset( this, options, i ); }); } if ( !elem || !elem.ownerDocument ) { return null; } //特殊处理body if ( elem === elem.ownerDocument.body ) { return jQuery.offset.bodyOffset( elem ); } //获得关于offset相关的浏览器特征 jQuery.offset.initialize(); var offsetParent = elem.offsetParent, prevOffsetParent = elem, doc = elem.ownerDocument, computedStyle, docElem = doc.documentElement, body = doc.body, defaultView = doc.defaultView, prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle, top = elem.offsetTop, left = elem.offsetLeft; while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) { //HTML,BODY,以及不具备CSS盒子模型的元素及display为fixed的元素没有offsetParent属性 if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { break; } computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle; top -= elem.scrollTop; left -= elem.scrollLeft; if ( elem === offsetParent ) { top += elem.offsetTop; left += elem.offsetLeft; //offset应该返回的是border-box,但在一些表格元素却没有计算它们的border值,需要自行添加 //在IE下表格元素的display为table,table-row与table-cell if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test(elem.nodeName)) ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevOffsetParent = offsetParent, offsetParent = elem.offsetParent; } //修正safari的错误 if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) { top += parseFloat( computedStyle.borderTopWidth ) || 0; left += parseFloat( computedStyle.borderLeftWidth ) || 0; } prevComputedStyle = computedStyle; } //最后加上body的偏移量 if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) { top += body.offsetTop; left += body.offsetLeft; } //使用固定定位,可能出现滚动条,我们要获得取大的滚动距离 if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) { top += Math.max( docElem.scrollTop, body.scrollTop ); left += Math.max( docElem.scrollLeft, body.scrollLeft ); } return { top: top, left: left }; };
如上所示,这种通过累加方式获得元素位置的方法非常不得人心,况且我还没有把它的辅助函数写出来呢!如果不是借助于框架,没有人愿意写这么一大堆的函数来获取这两个值。但是,getBoundingClientRect的出现与运用让情况大为改观,乐得John Resig专门写了篇文章大加赞赏此方法。他的博客中间贴了一幅图,很直观地显示新方法在篇幅上的精简,更别提在性能上的改善了!下面是我的实现:
//by 司徒正美 coords = function( el ) {//取得元素(左上角)的坐标 if ( !el || !el.ownerDocument ) { return null; } var pos = { x:0, y:0 }, owner = el.ownerDocument; if(el.tagName === "BODY"){ pos.x = el.offsetTop; pos.y = body.offsetLeft; //http://hkom.blog1.fc2.com/?mode=m&no=750 body的偏移量是不包含margin的 if(parseFloat(dom.getStyle(el,"margin-top"))!== el.offsetTop){ pos.x += parseFloat( dom.getStyle(el, "margin-top") ) || 0; pos.y += parseFloat( dom.getStyle(el, "margin-left")) || 0; } return pos; }else if(owner.getBoxObjectFor){//火狐 return owner.getBoxObjectFor(el); }else if (el.getBoundingClientRect) { //如果支持getBoundingClientRect //我们可以通过getBoundingClientRect来获得元素相对于client的rect. //http://msdn.microsoft.com/en-us/library/ms536433.aspx var box = el.getBoundingClientRect(), root = owner.documentElement, body = owner.getElementsByTagName("body")[0], clientTop = root.clientTop || body.clientTop || 0, clientLeft = root.clientLeft || body.clientLeft || 0; // 加上document的scroll的部分尺寸到left,top中。 // IE中会自动加上2px的border,这里是去掉document的边框大小。 // http://msdn.microsoft.com/en-us/library/ms533564(VS.85).aspx pos.x = box.left + (self.pageYOffset || dom.feature.w3cBox && root.scrollTop || body.scrollTop ) - clientTop, pos.y = box.top + (self.pageXOffset || dom.feature.w3cBox && root.scrollLeft || body.scrollLeft) - clientLeft; } return pos; }
这只是CSSOM方法的运用的一个特例,在精确获得元素的样式时,我们会更频繁地运用到它们。在做特效时,那几大家族的出现次数就更频繁。感谢微软带给我们这些好东西。但如果你想弄清楚offsetWidth,clientWidth等的准确含义时,我建议还是请到火狐官网查看,微软现在连自己的东西都不能作主,这真可悲。
相关链接:
- http://www.alanamos.de/articles/firefox_2+3_offset_attributes.html
- http://www.w3.org/TR/cssom-view/
- http://www.gtalbot.org/BrowserBugsSection/MSIE8Bugs/CSSOM-offsetParent-prop.html
- http://www.java2s.com/Code/JavaScript/HTML/UsingtheoffsetParentProperty.htm