在進入源碼分析前,咱們先來點基礎知識。下面這張圖畫的是元素的盒式模型,這個沒有兼容性問題,有問題的是元素的寬高怎麼算。以寬度爲例,ff中 元素寬度=content寬度,而在ie中 元素寬度=content寬度+border寬度+padding寬度。IE8中加入了box-sizzing,該css屬性有兩個值:border-box、content-box分別對應ie和ff中元素寬度的工做方式。css
偏移量:offsetLeft、offsetTop、offsetWidth、offsetHeighthtml
offsetLeft:包含元素的左內邊框到元素的左外邊框之間的像素距離。前端
offsetTop:包含元素的上內邊框到元素的上外邊框之間的相續距離。node
offsetWidth:包括元素的內容區寬度、左右內邊距寬度、左右邊框寬度、垂直方向滾動條的寬度之和。web
offsetHeight:包括元素內容區高度、左右內邊距高度、左右邊框高度、水平方向滾動條的高度之和。瀏覽器
包含元素的引用在offsetParent屬性中,offsetParent屬性不必定與parentNode屬性相同,好比<td>的offsetParent是<table>而不是<tr>.app
客戶區大小:clientWidth、clientHeightdom
clientWidth:元素的內容區寬度+內邊距寬度ide
clientHeight:元素的內容區高度+內邊距高度函數
滾動大小:scrollTop、scrollLeft、scrollWidth、scrollHeight。滾動大小指的是包含滾動內容的元素大小。
scrollTop:被隱藏在內容區域上方的像素數。
scrollLeft:被隱藏在內容區域左側的像素數。
經過設置以上兩個屬性能夠改變元素的滾動位置。
scrollWidth:在沒有滾動條狀況下,元素的內容的寬度。
scrollHeight:在沒有滾動條狀況下,元素內容的高度。
以上基礎知識,對咱們分析dom-geometry模塊的代碼會有很多幫助。下面咱們進入源碼學習階段。
dom-geometry模塊封裝了許多跟盒式模型相關的函數,主要涉及:content、padding、border、margin四方面。在前面的幾篇文章中咱們屢次提到,前端js庫中對dom操做的封裝最終都是要用到DOM原生的API。在此模塊中,最經常使用的原生方法就是elemet.ownerDocument.defaultView.getComputedStyle和element.getBoundingClientRect。儘管這兩個方法都存在着兼容性問題,但咱們都有適當的方法來解決。
getComputedStyle方法已經在dom-style模塊中介紹過(ie中使用element.currentStyle其餘瀏覽器利用原生的getComputedStyle,在webkit中對於不在正常文檔流中的元素先改變display),這裏簡單看一下:
1 if(has("webkit")){ 2 getComputedStyle = function(/*DomNode*/ node){ 3 var s; 4 if(node.nodeType == 1){ 5 var dv = node.ownerDocument.defaultView; 6 s = dv.getComputedStyle(node, null); 7 if(!s && node.style){ 8 node.style.display = ""; 9 s = dv.getComputedStyle(node, null); 10 } 11 } 12 return s || {}; 13 }; 14 }else if(has("ie") && (has("ie") < 9 || has("quirks"))){ 15 getComputedStyle = function(node){ 16 // IE (as of 7) doesn't expose Element like sane browsers 17 // currentStyle can be null on IE8! 18 return node.nodeType == 1 /* ELEMENT_NODE*/ && node.currentStyle ? node.currentStyle : {}; 19 }; 20 }else{ 21 getComputedStyle = function(node){ 22 return node.nodeType == 1 /* ELEMENT_NODE*/ ? 23 node.ownerDocument.defaultView.getComputedStyle(node, null) : {}; 24 }; 25 } 26 style.getComputedStyle = getComputedStyle;
getComputedStyle獲得的某些計算後樣式是帶有單位的,咱們要把單位去掉。這裏依賴dom-style中的toPixelValue方法:
1 var toPixel; 2 if(!has("ie")){ 3 toPixel = function(element, value){ 4 // style values can be floats, client code may want 5 // to round for integer pixels. 6 return parseFloat(value) || 0; 7 }; 8 }else{ 9 toPixel = function(element, avalue){ 10 if(!avalue){ return 0; } 11 // on IE7, medium is usually 4 pixels 12 if(avalue == "medium"){ return 4; } 13 // style values can be floats, client code may 14 // want to round this value for integer pixels. 15 if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); } 16 var s = element.style, rs = element.runtimeStyle, cs = element.currentStyle, 17 sLeft = s.left, rsLeft = rs.left; 18 rs.left = cs.left; 19 try{ 20 // 'avalue' may be incompatible with style.left, which can cause IE to throw 21 // this has been observed for border widths using "thin", "medium", "thick" constants 22 // those particular constants could be trapped by a lookup 23 // but perhaps there are more 24 s.left = avalue; 25 avalue = s.pixelLeft; 26 }catch(e){ 27 avalue = 0; 28 } 29 s.left = sLeft; 30 rs.left = rsLeft; 31 return avalue; 32 }; 33 } 34 style.toPixelValue = toPixel;
函數有點複雜,對於ie瀏覽器只要看懂這句就行:if(avalue.slice && avalue.slice(-2) == 'px'){ return parseFloat(avalue); }
回到dom-geometry的源碼,geom.boxModel變量表明當前瀏覽器中對元素使用的盒式模型,默認爲content-box,同時判斷了ie瀏覽器下的狀況:
var geom = { // summary: // This module defines the core dojo DOM geometry API. }; // can be either: // "border-box" // "content-box" (default) geom.boxModel = "content-box"; if(has("ie") /*|| has("opera")*/){ // client code may have to adjust if compatMode varies across iframes geom.boxModel = document.compatMode == "BackCompat" ? "border-box" : "content-box"; }
接下來的幾個函數比較簡單、基礎,經過getComputedStyle都能直接拿到相應屬性:
getPadExtents():getComputedStyle後獲得paddingLeft、paddingRight、paddingTop、paddingBottom
1 geom.getPadExtents = function getPadExtents(/*DomNode*/ node, /*Object*/ computedStyle){ 2 3 node = dom.byId(node); 4 var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue, 5 l = px(node, s.paddingLeft), t = px(node, s.paddingTop), r = px(node, s.paddingRight), b = px(node, s.paddingBottom); 6 return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; 7 };
getBorderExtents():getComputedStyle後獲得borderLeftWidth、borderRightWidth、borderTopWidth、borderBottomWidth;同時若是border-style設置爲none,border寬度爲零
1 geom.getBorderExtents = function getBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){ 2 node = dom.byId(node); 3 var px = style.toPixelValue, s = computedStyle || style.getComputedStyle(node), 4 l = s.borderLeftStyle != none ? px(node, s.borderLeftWidth) : 0, 5 t = s.borderTopStyle != none ? px(node, s.borderTopWidth) : 0, 6 r = s.borderRightStyle != none ? px(node, s.borderRightWidth) : 0, 7 b = s.borderBottomStyle != none ? px(node, s.borderBottomWidth) : 0; 8 return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; 9 };
getPadBorderExtents():經過上兩個方法,pad+border
1 geom.getPadBorderExtents = function getPadBorderExtents(/*DomNode*/ node, /*Object*/ computedStyle){ 2 node = dom.byId(node); 3 var s = computedStyle || style.getComputedStyle(node), 4 p = geom.getPadExtents(node, s), 5 b = geom.getBorderExtents(node, s); 6 return { 7 l: p.l + b.l, 8 t: p.t + b.t, 9 r: p.r + b.r, 10 b: p.b + b.b, 11 w: p.w + b.w, 12 h: p.h + b.h 13 }; 14 };
getMarginExtents():getComputedStyle後獲得marginLeft、marginRight、marginTop、marginBottom
1 geom.getMarginExtents = function getMarginExtents(node, computedStyle){ 2 node = dom.byId(node); 3 var s = computedStyle || style.getComputedStyle(node), px = style.toPixelValue, 4 l = px(node, s.marginLeft), t = px(node, s.marginTop), r = px(node, s.marginRight), b = px(node, s.marginBottom); 5 return {l: l, t: t, r: r, b: b, w: l + r, h: t + b}; 6 };
下面的幾個函數稍微有點複雜
getMarginBox()這個方法返回一個對象
{
t: 父元素上內邊框到元素上外邊距的距離,
l: 父元素左內邊框到元素左外邊距的距離,
w: 元素左外邊距到右外邊距的距離,
h: 元素上外邊距到下外邊距的距離
}
這個函數中主要用到上文提到的偏移量,正常狀況下:
t = offsetTop,
l = offsetLeft,
w = offsetWidth + marginExtents.w,
h = offsetHeight + marginExtents.h
在這個函數中有幾個兼容性問題:
一、在firefox中,若是元素的overflow樣子的計算值不爲visible,那麼offsetLeft/offsetTop獲得的值是減去borderLeftStyle/borderTopStyle後的值。這應該是firefox的bug,因此咱們要對此進行修復。若是getComputedStyle中可以獲得left和top那就用這兩個屬性代替offsetLeft和offsetTop,不然計算parentNode的border寬度,手動加上這部分值
1 if(has("mozilla")){ 2 // Mozilla: 3 // If offsetParent has a computed overflow != visible, the offsetLeft is decreased 4 // by the parent's border. 5 // We don't want to compute the parent's style, so instead we examine node's 6 // computed left/top which is more stable. 7 var sl = parseFloat(s.left), st = parseFloat(s.top); 8 if(!isNaN(sl) && !isNaN(st)){ 9 l = sl; 10 t = st; 11 }else{ 12 // If child's computed left/top are not parseable as a number (e.g. "auto"), we 13 // have no choice but to examine the parent's computed style. 14 if(p && p.style){ 15 pcs = style.getComputedStyle(p); 16 if(pcs.overflow != "visible"){ 17 l += pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; 18 t += pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; 19 } 20 } 21 } 22 }
二、在IE8和opera中狀況正好相反,offsetLeft/offsetTop包含了父元素的邊框,這裏咱們須要把他們減去
1 if(has("opera") || (has("ie") == 8 && !has("quirks"))){ 2 // On Opera and IE 8, offsetLeft/Top includes the parent's border 3 if(p){ 4 pcs = style.getComputedStyle(p); 5 l -= pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; 6 t -= pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; 7 } 8 }
真個函數代碼以下:
1 geom.getMarginBox = function getMarginBox(/*DomNode*/ node, /*Object*/ computedStyle){ 2 node = dom.byId(node); 3 var s = computedStyle || style.getComputedStyle(node), me = geom.getMarginExtents(node, s), 4 l = node.offsetLeft - me.l, t = node.offsetTop - me.t, p = node.parentNode, px = style.toPixelValue, pcs; 5 if(has("mozilla")){ 6 var sl = parseFloat(s.left), st = parseFloat(s.top); 7 if(!isNaN(sl) && !isNaN(st)){ 8 l = sl; 9 t = st; 10 }else{ 11 if(p && p.style){ 12 pcs = style.getComputedStyle(p); 13 if(pcs.overflow != "visible"){ 14 l += pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; 15 t += pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; 16 } 17 } 18 } 19 }else if(has("opera") || (has("ie") == 8 && !has("quirks"))){ 20 if(p){ 21 pcs = style.getComputedStyle(p); 22 l -= pcs.borderLeftStyle != none ? px(node, pcs.borderLeftWidth) : 0; 23 t -= pcs.borderTopStyle != none ? px(node, pcs.borderTopWidth) : 0; 24 } 25 } 26 return {l: l, t: t, w: node.offsetWidth + me.w, h: node.offsetHeight + me.h}; 27 };
getContentBox()函數返回以下對象:
{
l: 元素左內邊距,
t: 元素上內邊距,
w: 元素內容區的寬度,
h: 元素內容區的高度
}
對象中的w和h與元素的盒式模型無關。之內容區的寬高都有兩套方案:clientWidth-padingWidth或者offsetWidth-paddingWidth-borderWidth,下面是函數的源碼:
1 geom.getContentBox = function getContentBox(node, computedStyle){ 2 // summary: 3 // Returns an object that encodes the width, height, left and top 4 // positions of the node's content box, irrespective of the 5 // current box model. 6 // node: DOMNode 7 // computedStyle: Object? 8 // This parameter accepts computed styles object. 9 // If this parameter is omitted, the functions will call 10 // dojo/dom-style.getComputedStyle to get one. It is a better way, calling 11 // dojo/dom-style.getComputedStyle once, and then pass the reference to this 12 // computedStyle parameter. Wherever possible, reuse the returned 13 // object of dojo/dom-style.getComputedStyle(). 14 15 // clientWidth/Height are important since the automatically account for scrollbars 16 // fallback to offsetWidth/Height for special cases (see #3378) 17 node = dom.byId(node); 18 var s = computedStyle || style.getComputedStyle(node), w = node.clientWidth, h, 19 pe = geom.getPadExtents(node, s), be = geom.getBorderExtents(node, s); 20 if(!w){ 21 w = node.offsetWidth; 22 h = node.offsetHeight; 23 }else{ 24 h = node.clientHeight; 25 be.w = be.h = 0; 26 } 27 // On Opera, offsetLeft includes the parent's border 28 if(has("opera")){ 29 pe.l += be.l; 30 pe.t += be.t; 31 } 32 return {l: pe.l, t: pe.t, w: w - pe.w - be.w, h: h - pe.h - be.h}; 33 };
接下來有三個私有函數setBox、isButtonTag、usersBorderBox。
setBox忽略盒式模型,直接對元素樣式的width、height、left、top進行設置
1 function setBox(/*DomNode*/ node, /*Number?*/ l, /*Number?*/ t, /*Number?*/ w, /*Number?*/ h, /*String?*/ u){ 2 // summary: 3 // sets width/height/left/top in the current (native) box-model 4 // dimensions. Uses the unit passed in u. 5 // node: 6 // DOM Node reference. Id string not supported for performance 7 // reasons. 8 // l: 9 // left offset from parent. 10 // t: 11 // top offset from parent. 12 // w: 13 // width in current box model. 14 // h: 15 // width in current box model. 16 // u: 17 // unit measure to use for other measures. Defaults to "px". 18 u = u || "px"; 19 var s = node.style; 20 if(!isNaN(l)){ 21 s.left = l + u; 22 } 23 if(!isNaN(t)){ 24 s.top = t + u; 25 } 26 if(w >= 0){ 27 s.width = w + u; 28 } 29 if(h >= 0){ 30 s.height = h + u; 31 } 32 }
isButtonTag函數用來判斷元素是不是button按鈕,button元素多是直接的<button>標籤,也多是<input type="button">,因此要對着兩方面進行判斷
1 function isButtonTag(/*DomNode*/ node){ 2 // summary: 3 // True if the node is BUTTON or INPUT.type="button". 4 return node.tagName.toLowerCase() == "button" || 5 node.tagName.toLowerCase() == "input" && (node.getAttribute("type") || "").toLowerCase() == "button"; // boolean 6 }
usersBorderBox判斷元素的盒式模型是否爲border-box,三個方面:geom的boxModel是否爲border-box、元素是否爲table元素,元素是否爲button元素
1 function usesBorderBox(/*DomNode*/ node){ 2 // summary: 3 // True if the node uses border-box layout. 4 5 // We could test the computed style of node to see if a particular box 6 // has been specified, but there are details and we choose not to bother. 7 8 // TABLE and BUTTON (and INPUT type=button) are always border-box by default. 9 // If you have assigned a different box to either one via CSS then 10 // box functions will break. 11 12 return geom.boxModel == "border-box" || node.tagName.toLowerCase() == "table" || isButtonTag(node); // boolean 13 }
setContentSize方法,設置元素內容區的大小。若是元素盒式模式是border-box,則須要在參數傳入的width基礎上加上padding與border的寬度,不然直接設置width、height樣式。
1 geom.setContentSize = function setContentSize(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){ 2 // summary: 3 // Sets the size of the node's contents, irrespective of margins, 4 // padding, or borders. 5 // node: DOMNode 6 // box: Object 7 // hash with optional "w", and "h" properties for "width", and "height" 8 // respectively. All specified properties should have numeric values in whole pixels. 9 // computedStyle: Object? 10 // This parameter accepts computed styles object. 11 // If this parameter is omitted, the functions will call 12 // dojo/dom-style.getComputedStyle to get one. It is a better way, calling 13 // dojo/dom-style.getComputedStyle once, and then pass the reference to this 14 // computedStyle parameter. Wherever possible, reuse the returned 15 // object of dojo/dom-style.getComputedStyle(). 16 17 node = dom.byId(node); 18 var w = box.w, h = box.h; 19 if(usesBorderBox(node)){ 20 var pb = geom.getPadBorderExtents(node, computedStyle); 21 if(w >= 0){ 22 w += pb.w; 23 } 24 if(h >= 0){ 25 h += pb.h; 26 } 27 } 28 setBox(node, NaN, NaN, w, h); 29 };
setMarginBox方法,設置marginBox的寬度。該方法中不去判斷元素的盒式模型,width = w-padding - border -margin。經過這種方式直接設置元素的width或height屬性。這裏涉及的兼容性問題,主要對於低版本瀏覽器,因此不去分析他。
1 geom.setMarginBox = function setMarginBox(/*DomNode*/ node, /*Object*/ box, /*Object*/ computedStyle){ 2 // summary: 3 // sets the size of the node's margin box and placement 4 // (left/top), irrespective of box model. Think of it as a 5 // passthrough to setBox that handles box-model vagaries for 6 // you. 7 // node: DOMNode 8 // box: Object 9 // hash with optional "l", "t", "w", and "h" properties for "left", "right", "width", and "height" 10 // respectively. All specified properties should have numeric values in whole pixels. 11 // computedStyle: Object? 12 // This parameter accepts computed styles object. 13 // If this parameter is omitted, the functions will call 14 // dojo/dom-style.getComputedStyle to get one. It is a better way, calling 15 // dojo/dom-style.getComputedStyle once, and then pass the reference to this 16 // computedStyle parameter. Wherever possible, reuse the returned 17 // object of dojo/dom-style.getComputedStyle(). 18 19 node = dom.byId(node); 20 var s = computedStyle || style.getComputedStyle(node), w = box.w, h = box.h, 21 // Some elements have special padding, margin, and box-model settings. 22 // To use box functions you may need to set padding, margin explicitly. 23 // Controlling box-model is harder, in a pinch you might set dojo/dom-geometry.boxModel. 24 pb = usesBorderBox(node) ? nilExtents : geom.getPadBorderExtents(node, s), 25 mb = geom.getMarginExtents(node, s); 26 if(has("webkit")){ 27 // on Safari (3.1.2), button nodes with no explicit size have a default margin 28 // setting an explicit size eliminates the margin. 29 // We have to swizzle the width to get correct margin reading. 30 if(isButtonTag(node)){ 31 var ns = node.style; 32 if(w >= 0 && !ns.width){ 33 ns.width = "4px"; 34 } 35 if(h >= 0 && !ns.height){ 36 ns.height = "4px"; 37 } 38 } 39 } 40 if(w >= 0){ 41 w = Math.max(w - pb.w - mb.w, 0); 42 } 43 if(h >= 0){ 44 h = Math.max(h - pb.h - mb.h, 0); 45 } 46 setBox(node, box.l, box.t, w, h); 47 };
position()方法,主要使用node.getBoundingClientRect() ,這個方法獲得left、right、top、bottom。在老版本ie下,這個方法的基準點並非從(0,0)開始計算的,而是以(2,2)位基準點。因此ie中這個方法獲得的位置信息比實際位置多了兩個像素,咱們要把這兩個像素減掉。基準點位置偏移兩個像素,因此dcument.documentElement即<html>標籤的位置也不是0;因此咱們能夠利用document.documentElement.getBoundingClientRect().left/top獲得偏移量。減去偏移量就獲得了真正的位置。(偏移量問題在IE9已經修復了,而IE8標準模式是沒有這個問題,因此具體獲取偏移量的細節不討論)
1 geom.position = function(/*DomNode*/ node, /*Boolean?*/ includeScroll){ 2 // summary: 3 // Gets the position and size of the passed element relative to 4 // the viewport (if includeScroll==false), or relative to the 5 // document root (if includeScroll==true). 6 // 7 // description: 8 // Returns an object of the form: 9 // `{ x: 100, y: 300, w: 20, h: 15 }`. 10 // If includeScroll==true, the x and y values will include any 11 // document offsets that may affect the position relative to the 12 // viewport. 13 // Uses the border-box model (inclusive of border and padding but 14 // not margin). Does not act as a setter. 15 // node: DOMNode|String 16 // includeScroll: Boolean? 17 // returns: Object 18 19 node = dom.byId(node); 20 var db = win.body(node.ownerDocument), 21 ret = node.getBoundingClientRect(); 22 ret = {x: ret.left, y: ret.top, w: ret.right - ret.left, h: ret.bottom - ret.top}; 23 24 if(has("ie") < 9){ 25 // On IE<9 there's a 2px offset that we need to adjust for, see dojo.getIeDocumentElementOffset() 26 var offset = geom.getIeDocumentElementOffset(node.ownerDocument); 27 28 // fixes the position in IE, quirks mode 29 ret.x -= offset.x + (has("quirks") ? db.clientLeft + db.offsetLeft : 0); 30 ret.y -= offset.y + (has("quirks") ? db.clientTop + db.offsetTop : 0); 31 } 32 33 // account for document scrolling 34 // if offsetParent is used, ret value already includes scroll position 35 // so we may have to actually remove that value if !includeScroll 36 if(includeScroll){ 37 var scroll = geom.docScroll(node.ownerDocument); 38 ret.x += scroll.x; 39 ret.y += scroll.y; 40 } 41 42 return ret; // Object 43 };
normalizeEvent()方法主要針對鼠標位置信息layerX/layerY、pageX/pageY作修正;前者利用ie中的offsetX/offsetY便可,後者利用clientX+documentElement/body.scrollLeft - offset和clientY+documentElement/body.scrollTop - offset,offset便是上文提到的在ie中偏移量。
geom.normalizeEvent = function(event){ // summary: // Normalizes the geometry of a DOM event, normalizing the pageX, pageY, // offsetX, offsetY, layerX, and layerX properties // event: Object if(!("layerX" in event)){ event.layerX = event.offsetX; event.layerY = event.offsetY; } if(!has("dom-addeventlistener")){ // old IE version // FIXME: scroll position query is duped from dojo/_base/html to // avoid dependency on that entire module. Now that HTML is in // Base, we should convert back to something similar there. var se = event.target; var doc = (se && se.ownerDocument) || document; // DO NOT replace the following to use dojo/_base/window.body(), in IE, document.documentElement should be used // here rather than document.body var docBody = has("quirks") ? doc.body : doc.documentElement; var offset = geom.getIeDocumentElementOffset(doc); event.pageX = event.clientX + geom.fixIeBiDiScrollLeft(docBody.scrollLeft || 0, doc) - offset.x; event.pageY = event.clientY + (docBody.scrollTop || 0) - offset.y; } };
一個周的學習研究結束,若是您以爲這篇文章對您有幫助,請不吝點擊下方推薦