jQuery源码解析之width

3次阅读

共计 12809 个字符,预计需要花费 33 分钟才能阅读完成。

一、在讲之前,先弄清 boxSizing 属性
(1)box-sizing 是默认值 “content-box”

<body>
<script src="jQuery.js"></script>
<div id="pTwo"
     style="width: 55px;
     border:1px red solid;"> 这是 divTwo</div>
<script>
  $("#pTwo").width() //55
</script>
</body>

$().width()的值是 55

(2)box-sizing 是 “border-box”

<div id="pTwo"
     style="width: 55px;
     box-sizing: border-box;
     border:1px red solid;"> 这是 divTwo</div>

$().width()的值是 53

因为 border-box 是包括 border、padding、content 的,而 content-box 只包括 content。
可想而知,jQuery 的$().width() 中也包含了对 borderBox 的判断。

  • 注意下 div 标签的默认值

二、$().width()
作用:
获取目标元素的宽度

源码:

  // 源码 7033 行
  //$.each(obj,callback(index,item){})
  jQuery.each([ "height", "width"], function(i, dimension) {
    //i:0 dimension:height
    //i:1 dimension:width

    //cssHooks 是用来定义 style 方法的
    jQuery.cssHooks[dimension] = {
      // 读
      //$().width()
      // 参数:elem: 目标 DOM 元素 /computed:true/extra:"content"
      get: function(elem, computed, extra) {console.log(elem, computed, extra,'extra7040')
        if (computed) {

          // 某些元素是有尺寸的信息的,如果我们隐式地显示它们,前提是它必须有一个 display 值
          // Certain elements can have dimension info if we invisibly show them
          // but it must have a current display style that would benefit

          // 上面这句话的意思是,某个元素用 display:none,将它从页面上去掉了,此时是获取不到它的宽度的
          // 如果要获取它的宽度的话,需要隐式地显示它们,比如 display:absolute,visible:hidden
          // 然后再去获取它的宽度

          // block:false
          // none:true
          // rdisplayswap 的作用是检测 none 和 table 开头的
          return rdisplayswap.test(jQuery.css( elem, "display") ) &&

          // 兼容性的考虑,直接看 getWidthOrHeight

          // Support: Safari 8+
          // Table columns in Safari have non-zero offsetWidth & zero
          // getBoundingClientRect().width unless display is changed.
          // Support: IE <=11 only
          // Running getBoundingClientRect on a disconnected node
          // in IE throws an error.

          // display 为 none 的话,elem.getBoundingClientRect().width=0
          // elem.getClientRects() 返回 CSS 边框的集合
          // https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getClientRects
          (!elem.getClientRects().length || !elem.getBoundingClientRect().width) ?
            swap(elem, cssShow, function() {return getWidthOrHeight( elem, dimension, extra);
            } ) :
            //$().width()情况
            //dimension:width/extra:"content"
            getWidthOrHeight(elem, dimension, extra);
        }
      },
   };
} );

解析:
(1)box-sizing 是默认值,并且 display 不为 none
rdisplayswap
作用:
检测目标元素的 display 属性的值 是否为 none 或以 table 开头

    // 检测 display 的值是否为 none 或以 table 开头
    // Swappable if display is none or starts with table
    // 除了 "table", "table-cell", "table-caption"
    // except "table", "table-cell", or "table-caption"
    // display 的值,请访问 https://developer.mozilla.org/en-US/docs/CSS/display
    // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
    // 源码 6698 行
var rdisplayswap = /^(none|table(?!-c[ea]).+)/,

如果 displaynone的话,就会调用 swap() 方法,反之,就直接调用 getWidthOrHeight() 方法

getWidthOrHeight()
作用:
获取 widthheight的值

 // 获取 width 或 height
  //dimension:width/extra:"content"
  // 源码 6823 行
  function getWidthOrHeight(elem, dimension, extra) {

    // Start with computed style
    var styles = getStyles(elem),
      val = curCSS(elem, dimension, styles),
      // 判断 box-sizing 的值是否 是 border-box
      // 如果启用了 box-sizing,js 的 width 是会算上 margin、border、padding 的
      // 如果不启用的话,js 的 width 只会算 content
      //jQuery 的 width 自始至终都是算的 content
      isBorderBox = jQuery.css(elem, "boxSizing", false, styles) === "border-box",

      valueIsBorderBox = isBorderBox;

    // 火狐兼容性处理,可不看
    // Support: Firefox <=54
    // Return a confounding non-pixel value or feign ignorance, as appropriate.
    if (rnumnonpx.test( val) ) {if ( !extra) {return val;}
      val = "auto";
    }

    // 通过 getComputedStyle 检查 style 属性,并返回可靠的 style 属性,这样可以防止浏览器返回不可靠的值
    // Check for style in case a browser which returns unreliable values
    // for getComputedStyle silently falls back to the reliable elem.style
    valueIsBorderBox = valueIsBorderBox &&
      (support.boxSizingReliable() || val === elem.style[dimension] );
    console.log(valueIsBorderBox,'valueIsBorderBox6853')
    // Fall back to offsetWidth/offsetHeight when value is "auto"
    // This happens for inline elements with no explicit setting (gh-3571)
    // Support: Android <=4.1 - 4.3 only
    // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602)
    if ( val === "auto" ||
      !parseFloat(val) && jQuery.css(elem, "display", false, styles) === "inline" ) {val = elem[ "offset" + dimension[ 0].toUpperCase() + dimension.slice( 1) ];
      console.log(val,'val6862')
      // offsetWidth/offsetHeight provide border-box values
      valueIsBorderBox = true;
    }
    // Normalize "" and auto
    // 55px
    val = parseFloat(val) || 0;
    console.log(val,extra,'val6869')
    // Adjust for the element's box model
    return ( val +
      boxModelAdjustment(
        //DOM 节点
        elem,
        //width
        dimension,
        //content
        extra || (isBorderBox ? "border" : "content"),
        //true/false
        valueIsBorderBox,
        //styles
        styles,
        //55
        // Provide the current computed size to request scroll gutter calculation (gh-3589)
        val
      )
    ) + "px";
  }

getWidthOrHeight() 里面有好多方法,我们一一来解析:

getStyles(elem)
作用:
获取该 DOM 元素的所有 css 属性的值

  // 获取该 DOM 元素的所有 css 属性的值
  // 源码 6501 行
  var getStyles = function(elem) {
    // 兼容性处理,旨在拿到正确的 view
    // Support: IE <=11 only, Firefox <=30 (#15098, #14150)
    // IE throws on elements created in popups
    // FF meanwhile throws on frame elements through "defaultView.getComputedStyle"
    var view = elem.ownerDocument.defaultView;

    if (!view || !view.opener) {view = window;}
    // 获取所有 CSS 属性的值
    return view.getComputedStyle(elem);
  };

可以看到,本质是调用了 getComputedStyle() 方法。

curCSS(elem, dimension, styles)
作用:
获取元素的当前属性的值

// 获取元素的当前属性的值
  // elem, "position"
  // elem,width,styles
  // 源码 6609 行
  function curCSS(elem, name, computed) {

    var width, minWidth, maxWidth, ret,

      // Support: Firefox 51+
      // Retrieving style before computed somehow
      // fixes an issue with getting wrong values
      // on detached elements
      style = elem.style;
    // 获取 elem 所有的样式属性
    computed = computed || getStyles(elem);
    // console.log(computed,'computed6621')
    // getPropertyValue is needed for:
    //   .css('filter') (IE 9 only, #12537)
    //   .css('--customProperty) (#3144)
    if (computed) {
      // 返回元素的属性的当前值
      //position:static
      //top:0px
      //left:0px
      ret = computed.getPropertyValue(name) || computed[name];
      console.log(ret,'ret6627')
      // 如果目标属性值为空并且目标元素不在目标元素所在的文档内(感觉这种情况好奇怪)if (ret === "" && !jQuery.contains( elem.ownerDocument, elem) ) {
        // 使用 jQuery.style 方法来获取目标元素的属性值
        ret = jQuery.style(elem, name);
      }

      // A tribute to the "awesome hack by Dean Edwards"
      // Android Browser returns percentage for some values,
      // but width seems to be reliably pixels.
      // This is against the CSSOM draft spec:
      // https://drafts.csswg.org/cssom/#resolved-values
      // 当属性设置成数值时,安卓浏览器会返回一些百分比,但是宽度是像素显示的
      // 这违反了 CSSOM 草案规范
      // 所以以下方法是修复不规范的 width 属性的
      if (!support.pixelBoxStyles() && rnumnonpx.test(ret) && rboxStyle.test(name) ) {

        // Remember the original values
        width = style.width;
        minWidth = style.minWidth;
        maxWidth = style.maxWidth;

        // Put in the new values to get a computed value out
        style.minWidth = style.maxWidth = style.width = ret;
        ret = computed.width;

        // Revert the changed values
        style.width = width;
        style.minWidth = minWidth;
        style.maxWidth = maxWidth;
      }
    }

    return ret !== undefined ?
      // 兼容性,IE 下返回的 zIndex 的值是数字,// 而使用 jQuery 获取的属性都是返回字符串
      // Support: IE <=9 - 11 only
      // IE returns zIndex value as an integer.
      ret + "" :
      ret;
  }

可以看到,curCSS本质是调用了 computed.getPropertyValue(name) 方法,也就是说我们可以这样去获取目标元素的属性值:

let a=document.getElementById("pTwo")
a.ownerDocument.defaultView.getComputedStyle(a).getPropertyValue('width')
//55px

目标元素的所属 view,调用 getComputedStyle() 方法,获取目标元素的所有 CSS 属性,再调用 getPropertyValue('width'),获取目标width 的属性值,为 55px

注意:无论 box-sizing 的值是 border-box 还是 content-box,上面的方法获取的width 值都是55px,这是不符合 CSS3 盒子模型的,所以 jQuery 拿到该值后,还会继续处理。

boxModelAdjustment
因为这里讨论的是情况一,所以 boxModelAdjustment() 会直接返回 0

综上:当 box-sizing 是默认值,并且 display 不为 none 时,返回的 width 是:

parseFloat(a.ownerDocument.defaultView.getComputedStyle(a).getPropertyValue('width')) //55

(2)box-sizing 值为 border-box

<div id="pTwo"
     style="width: 55px;
     margin-left:2px;
     padding-left: 2px;
     box-sizing: border-box;
     border:1px red solid;"> 这是 divTwo</div>
$("#pTwo").width() //51
document.getElementById("pTwo").style.width //55px

可以看到,原生 js 获取 width 是不遵循 CSS3 盒子规范的。

borderBox 的判断在 getWidthOrHeight() 方法中,直接看过去:

  // 获取 width 或 height
  //dimension:width/extra:"content"
  // 源码 6823 行
  function getWidthOrHeight(elem, dimension, extra) {
    xxx
    ...
    var styles = getStyles(elem),
      //true
      isBorderBox = jQuery.css(elem, "boxSizing", false, styles) === "border-box",
      //true
      valueIsBorderBox = isBorderBox;
    xxx
    ...
    valueIsBorderBox = valueIsBorderBox &&
      //val 值是通过 a.ownerDocument.defaultView.getComputedStyle(a).getPropertyValue('width')得出的
      // 但又通过 js 原生的 style.width 来取值并与 val 相比较
      (support.boxSizingReliable() || val === elem.style[dimension] );
    console.log(val === elem.style[ dimension],'valueIsBorderBox6853')
 
    // 55
    val = parseFloat(val) || 0;
    // Adjust for the element's box model
    return ( val +
      //borderBox 走这里
      boxModelAdjustment(
        //DOM 节点
        elem,
        //width
        dimension,
        //content
        extra || (isBorderBox ? "border" : "content"),
        //true/false
        valueIsBorderBox,
        //styles
        styles,
        //55
        // Provide the current computed size to request scroll gutter calculation (gh-3589)
        val
      )
    ) + "px";
  }

boxModelAdjustment():
作用:
集中处理 borderBox 的情况

// 参数说明://elem:DOM 节点 /dimension:width/box:content/isBorderBox:true/false/styles:styles/computedVal:55
  // 源码 6758 行
  function boxModelAdjustment(elem, dimension, box, isBorderBox, styles, computedVal) {
    var i = dimension === "width" ? 1 : 0,
      extra = 0,
      delta = 0;

    // 如果 boxSizing 的属性值,而不是 borderBox 的话,就直接返回 0
    // Adjustment may not be necessary
    if (box === ( isBorderBox ? "border" : "content") ) {console.log('content1111','content6768')
      return 0;
    }
    // 小技巧
    //i 的初始值是 0/1
    // 然后 cssExpand = ["Top", "Right", "Bottom", "Left"]
    for (; i < 4; i += 2) {

      // Both box models exclude margin
      if (box === "margin") {//var cssExpand = [ "Top", "Right", "Bottom", "Left"];
        //width 的话,就是 marginRight/marginLeft
        //height 的话,就是 marginTop/marginBottom
        //jQuery.css(elem, box + cssExpand[ i], true, styles ) 的意思就是
        // 返回 marginRight/marginLeft/marginTop/marginBottom 的数字,并给 delta 加上
        delta += jQuery.css(elem, box + cssExpand[ i], true, styles );
      }

      // If we get here with a content-box, we're seeking"padding"or"border"or"margin"
      // 如果不是 borderBox 的话
      if (!isBorderBox) {

        // Add padding
        // 添加 padding-xxx
        delta += jQuery.css(elem, "padding" + cssExpand[ i], true, styles );

        // For "border" or "margin", add border
        if (box !== "padding") {delta += jQuery.css( elem, "border" + cssExpand[ i] + "Width", true, styles );

          // But still keep track of it otherwise
        } else {extra += jQuery.css( elem, "border" + cssExpand[ i] + "Width", true, styles );
        }

        // If we get here with a border-box (content + padding + border), we're seeking"content" or
        // "padding" or "margin"
      } else {
        // 去掉 padding
        // For "content", subtract padding
        if (box === "content") {
          //width,去掉 paddingLeft,paddingRight 的值
          delta -= jQuery.css(elem, "padding" + cssExpand[ i], true, styles );
        }

        // For "content" or "padding", subtract border
        // 去掉 borderXXXWidth
        if (box !== "margin") {
          //width,去掉 borderLeftWidth,borderRightWidth 的值
          delta -= jQuery.css(elem, "border" + cssExpand[ i] + "Width", true, styles );
        }
      }
    }

    // Account for positive content-box scroll gutter when requested by providing computedVal
    if (!isBorderBox && computedVal >= 0) {

      // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border
      // Assuming integer scroll gutter, subtract the rest and round down
      delta += Math.max( 0, Math.ceil(
        // 就是将 dimension 的首字母做个大写
        elem["offset" + dimension[ 0].toUpperCase() + dimension.slice( 1) ] -
        computedVal -
        delta -
        extra -
        0.5
      ) );
    }

    return delta;
  }

可以看到,isBorderBox 为 true 的话,会执行下面两段代码:

if (box === "content") {
   //width,去掉 paddingLeft,paddingRight 的值
   delta -= jQuery.css(elem, "padding" + cssExpand[ i], true, styles );
}
 if (box !== "margin") {
    //width,去掉 borderLeftWidth,borderRightWidth 的值
    delta -= jQuery.css(elem, "border" + cssExpand[ i] + "Width", true, styles );
}

去除了 paddingLeftpaddingRightborderLeftWidthborderRightWidth,并最终返回值

二、$().width(xxx)
作用:
设置目标元素的宽度

源码:


  // 源码 7033 行
  //$.each(obj,callback(index,item){})
  jQuery.each([ "height", "width"], function(i, dimension) {
    //i:0 dimension:height
    //i:1 dimension:width

    //cssHooks 是用来定义 style 方法的
    jQuery.cssHooks[dimension] = {
      // 写
      //$().width(55)
      //elem:DOM 节点,value:55,extra:content
      set: function(elem, value, extra) {
        var matches,
          styles = getStyles(elem),
          isBorderBox = jQuery.css(elem, "boxSizing", false, styles) === "border-box",
          //-4
          subtract = extra && boxModelAdjustment(
            elem,
            dimension,
            extra,
            isBorderBox,
            styles
          );

        // 如果是 borderBox 的话,通过 offset 计算的尺寸是不准的,// 所以要假设成 content-box 来获取 border 和 padding
        // Account for unreliable border-box dimensions by comparing offset* to computed and
        // faking a content-box to get border and padding (gh-3699)
        //true true 'static'
        // 调整 subtract
        if (isBorderBox && support.scrollboxSize() === styles.position ) {
          subtract -= Math.ceil(elem[ "offset" + dimension[ 0].toUpperCase() + dimension.slice( 1) ] -
            parseFloat(styles[ dimension] ) -
            boxModelAdjustment(elem, dimension, "border", false, styles) -
            0.5
          );
          console.log(subtract,'subtract7169')
        }
        // 如果需要进行值调整,则转换为像素
        // Convert to pixels if value adjustment is needed
        // 如果是 borderBox 并且 value 的单位不是 px,则会转换成像素
        if (subtract && ( matches = rcssNum.exec( value) ) &&
          (matches[ 3] || "px" ) !== "px" ) {elem.style[ dimension] = value;
          value = jQuery.css(elem, dimension);
        }
        //59px
        return setPositiveNumber(elem, value, subtract);
      }
  };
} );

解析:
(1)整体上看,实际上两个 if,最后再 return 一个 setPositiveNumber() 方法

(2)注意subtract ,如果有 borderBox 属性,并且 borderWidth、padding 有值的话,subtract 一般为负数,比如下面的例子,subtract = -4

<div id="pTwo"
     style="width: 55px;
     margin-left:2px;
     padding-left: 2px;
     box-sizing: border-box;
     /*box-sizing: content-box;*/
     /*display: none;*/
     border:1px red solid;">
  这是 divTwo
</div>

$("#pTwo").width(55)

反之则会是 0

(3)两个 if 我试了下,都会去执行,所以直接看的setPositiveNumber ()

setPositiveNumber:
作用:
设置真正的 width 值

  function setPositiveNumber(elem, value, subtract) {
    // 标准化相对值
    // Any relative (+/-) values have already been
    // normalized at this point

    //[
    // "55px",
    // undefined,
    // "55",
    // "px",
    // index: 0,
    // input: "55px",
    // groups: undefined,
    // index: 0
    // input: "55px"
    // ]
    var matches = rcssNum.exec(value);
    console.log(matches,( subtract || 0),'matches6760')
    return matches ?
      //(0,55-(-4))+'px'
      //Math.max(a,b) 返回两个指定的数中带有较大的值的那个数
      // Guard against undefined "subtract", e.g., when used as in cssHooks
      Math.max(0, matches[ 2] - (subtract || 0) ) + (matches[ 3] || "px" ) :
      value;
  }

如果是 borderBox,width 会设置成 59px(虽然表面上开发者设置的是$("#pTwo").width(55)),反之,则是 55px

总结:
1、$().width()
(1)不是borderBox
$().width()=parseFloat(elem.ownerDocument.defaultView.getComputedStyle(elem).getPropertyValue('width'))

(2)是 borderBox()
在(1)的基础上执行 boxModelAdjustment() 方法,去除 borderWidth、padding

2、$().width(xxx)
(1)不是borderBox
width=xxx
(2)是borderBox
width=xxx+ setPositiveNumber()


(完)

正文完
 0