jQuery原始碼解析之position()

進擊的小進進發表於2019-05-05

position()
作用:
返回被選元素相對於父元素(parent)的偏移座標

使用:
直接呼叫$().position()即可,該方法沒有 arguments(引數物件)

<body>
<script src="jQuery.js"></script>
<p id="pTwo">這是divTwo</p>
<script>
  $("#pTwo").position() //{top: 0, left: 8}
</script>
</body>
複製程式碼

原始碼:

  // 返回被選元素相對於父元素(parent)的偏移座標
    // 可以理解成被選元素設定為absolute,
    // 然後設定left、top的值就是相對於父元素的偏移座標
    // 原始碼10571行
    // position() relates an element's margin box to its offset parent's padding box
    // This corresponds to the behavior of CSS absolute positioning
    position: function() {
      // 如果DOM元素不存在,直接返回
      if ( !this0 ] ) {
        return;
      }

      var offsetParent, offset, doc,
        elem = this0 ],
        parentOffset = { top: 0, left: 0 };

      // position:fixed elements are offset from the viewport, which itself always has zero offset
      // position:fixed的元素,是相對於瀏覽器視窗進行定位的,
      // 所以它的偏移就是getBoundingClientRect(),即獲取某個元素相對於視窗的位置
      if ( jQuery.css( elem, "position" ) === "fixed" ) {

        // Assume position:fixed implies availability of getBoundingClientRect
        offset = elem.getBoundingClientRect();

      }
      // 除去position是fixed的情況
      else {
        // 獲取被選元素相對於文件(document)的偏移座標
        offset = this.offset();

        // Account for the *real* offset parent, which can be the document or its root element
        // when a statically positioned element is identified
        doc = elem.ownerDocument;
        //定位目標元素的父元素(position不為static的元素)
        offsetParent = elem.offsetParent || doc.documentElement;
        // 如果父元素是<body>/<html>的話,將父元素重新定位為它們的父元素
        // body的父元素是html,html的父元素是document
        while ( offsetParent &&
        ( offsetParent === doc.body || offsetParent === doc.documentElement ) &&
        jQuery.css( offsetParent, "position" ) === "static" ) {

          offsetParent = offsetParent.parentNode;
        }
        // 如果定位父元素存在,並且不等於目標元素,並且定位元素型別是 "元素型別"
        if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 ) {

          // Incorporate borders into its offset, since they are outside its content origin
          parentOffset = jQuery( offsetParent ).offset();
          // 這兩行程式碼的意思是父元素的offset()要從padding算起,不包括border
          // 所以需要去掉border
          // jQuery.css( element, "borderTopWidth", true )的 true 表示返回數字,而不帶單位 px
          parentOffset.top += jQuery.css( offsetParent, "borderTopWidth"true );
          parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth"true );
        }
      }

      // Subtract parent offsets and element margins
      // 可以看出,$().position()的本質是目標元素的offset()減去父元素的offset(),同時還要算上目標元素的margin,因為盒子模型(關鍵)。
      //(注意:offset()的本質是getBoundingClientRect()的top、left + pageYOffset、pageXOffset)
      return {
        top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop"true ),
        left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft"true )
      };
    },
複製程式碼

解析:
整體上看,是一個 if(...fixed) { } esle { } 語句
(1)if ( jQuery.css( elem, "position" ) === "fixed" )

if ( jQuery.css( elem, "position" ) === "fixed" ) {
        // Assume position:fixed implies availability of getBoundingClientRect
        offset = elem.getBoundingClientRect();
}
return {
        top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop"true ),
        left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft"true )
      };
複製程式碼

由於position:fixed的元素,是相對於瀏覽器視窗進行定位的,所以它的偏移就是getBoundingClientRect(),即獲取某個元素相對於視窗的位置。

注意:
① getBoundingClientRect() 計算的是目標元素的border的位置(左上角),是不包括margin的
② 如果不加上margin的話(程式碼是通過減去,來算上margin的),是不準確的,看下圖

所以原始碼最後會:

- jQuery.css( elem, "marginTop"true )
- jQuery.css( elem, "marginLeft"true )
複製程式碼

(2)jQuery.css( elem, "width", true )
true的作用是返回該屬性的數字,而不帶單位 px

(3)定位父元素存在,並且不等於目標元素,並且定位元素型別是 "元素型別"的話

if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 )
複製程式碼

是要減去border屬性的值的

parentOffset.top += jQuery.css( offsetParent, "borderTopWidth"true );
parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth"true );
複製程式碼

為啥?舉個例子:

let p=document.querySelector("#pTwo")
console.log(p.getBoundingClientRect(),'pTwo11'); //x:8,y:16
複製程式碼

設定 borderLeftWidth 為 8 畫素

let p=document.querySelector("#pTwo")
p.style.borderLeftWidth='8px'
console.log(p.getBoundingClientRect(),'pTwo11'); //x:8,y:16
複製程式碼

可以看到getBoundingClientRect()指定座標是到border上的,這是不準確的,因為在裡面的子元素的位置也會受父元素的border影響,所以父元素的座標需要越過border

綜上:
可以看出,$().position()的本質是目標元素的 offset() 減去父元素的 offset(),同時還要算上目標元素的 margin,算上父元素的border。
(注意:offset()的本質是getBoundingClientRect()的top、left + pageYOffset、pageXOffset)

Github:
github.com/AttackXiaoJ…


(完)

相關文章