jQuery 4.0 源码架构与工程实践-第 10 章 尺寸、几何与显隐
本章导览
| 小节 | 主题 | 关键源码 | 难度 |
|---|---|---|---|
| 10.1 | cssHooks 协议:get / set / expand |
6138 / 6257--6328 | ★★★★ |
| 10.2 | getWidthOrHeight:尺寸的六条路径 |
6061--6132 | ★★★★★ |
| 10.3 | boxModelAdjustment:四盒模型的加减法 |
5990--6059 | ★★★★★ |
| 10.4 | setPositiveNumber 与负边距 |
5978--5988 | ★★ |
| 10.5 | jQuery.fn.css 与 access 的第四态 |
6330--6353 | ★★ |
| 10.6 | 尺寸方法族:height / innerHeight / outerHeight |
9488--9534 | ★★★ |
| 10.7 | 显隐:isHiddenWithinTree / getDefaultDisplay / showHide |
4088--4288 | ★★★★ |
| 10.8 | 几何:offset / position / offsetParent / setOffset |
9289--9453 | ★★★★ |
| 10.9 | 滚动:scrollLeft / scrollTop |
9455--9485 | ★★ |
| 10.10 | 陷阱与边界(18 条) | --- | ★★★★ |
| 10.11 | 全章总结 + 附录 A/B/C | --- | ★★ |
本章是第 09 章的姊妹篇。第 09 章讲样式读写的双通道 (
getStyles/curCSS/jQuery.style/adjustCSS/jQuery.css),本章讲物理量的计算:尺寸怎么量、盒模型怎么加减、元素在哪里、怎么显示和隐藏。
本章的四个核心函数构成一条严密的链条:
scss
jQuery.fn.width(...) ← 用户 API
↓ access + extra
jQuery.css( elem, "width", "border" ) ← 读通道(第 09 章 9.10)
↓ cssHooks.width.get
getWidthOrHeight( elem, "width", "border" ) ← 尺寸主入口(10.2)
↓
boxModelAdjustment( ... ) ← 盒模型算术(10.3)
↓
setPositiveNumber( ... ) ← 写通道的值钳制(10.4)
以及三条独立的支线:showHide(显隐,10.7)、offset/position(几何,10.8)、scrollLeft/scrollTop(滚动,10.9)。
10.1 cssHooks 协议:get / set / expand
js
// Add in style property hooks for overriding the default
// behavior of getting and setting a style property
cssHooks: {},
(6136--6138)
jQuery.cssHooks 是一个空对象起步的扩展点 。jQuery 内建往里塞三个系列的钩子,第三方插件可以继续加。它是 jQuery 样式模块唯一公开的扩展机制。
钩子的三个可选方法
| 方法 | 签名 | 返回 undefined 的含义 |
调用方 |
|---|---|---|---|
get |
( elem, computed, extra ) |
我不处理,请走默认路径 | jQuery.css 6234 / jQuery.style 6207 |
set |
( elem, value, extra ) |
我已写入,请不要再写 | jQuery.style 6194 |
expand |
( value ) → { prop: value, ... } |
--- | 动画模块的 propFilter(第 14 章,6852 附近) |
注意:没有任何校验 。jQuery 不检查钩子的形状,只在需要时用 "get" in hooks / "set" in hooks 判断。这是一个宽松的鸭子类型协议。
内建钩子系列一:width / height(6257--6300)
js
jQuery.each( [ "height", "width" ], function( _i, dimension ) {
jQuery.cssHooks[ dimension ] = {
get: function( elem, computed, extra ) {
if ( computed ) {
// Elements with `display: none` can have dimension info if
// we invisibly show them.
return jQuery.css( elem, "display" ) === "none" ?
swap( elem, cssShow, function() {
return getWidthOrHeight( elem, dimension, extra );
} ) :
getWidthOrHeight( elem, dimension, extra );
}
},
set: function( elem, value, extra ) {
var matches,
styles = getStyles( elem ),
// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991)
isBorderBox = extra &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
subtract = extra ?
boxModelAdjustment(
elem,
dimension,
extra,
isBorderBox,
styles
) :
0;
// Convert to pixels if value adjustment is needed
if ( subtract && ( matches = rcssNum.exec( value ) ) &&
( matches[ 3 ] || "px" ) !== "px" ) {
elem.style[ dimension ] = value;
value = jQuery.css( elem, dimension );
}
return setPositiveNumber( elem, value, subtract );
}
};
} );
get 的逻辑:
computed为false→ 返回undefined(让jQuery.style读elem.style.width)computed为true→ 检查display是否为none- 是 → 用
swap临时显示(cssShow),读尺寸,再恢复 - 否 → 直接
getWidthOrHeight
- 是 → 用
这是 .width() 能对隐藏元素工作的全部秘密 。一个 display: none 的 div,.width() 仍然返回有意义的数字------因为 jQuery 会临时把它变成 position:absolute; visibility:hidden; display:block,量完再变回去。
代价:一次 swap 意味着一次强制同步布局(读 offsetWidth 会 flush 样式队列)。所以对隐藏元素读尺寸是昂贵操作,在循环里要避免。
set 的逻辑:
extra为空时不查boxSizing(gh-3991 注释:避免强制 reflow)------因为extra为空意味着用户只是.css( "width", 100 ),不需要盒模型换算subtract= 需要从目标值里减掉的"额外层"像素数- 单位统一化 :如果
subtract非零且传入值不是 px(比如50%),则先把值写进去,再用jQuery.css读回 px 值 。因为盒模型加减只能在 px 下进行------你不能从50%里减去20px setPositiveNumber( elem, value, subtract )做减法并保证非负
第 3 步是这段代码的精髓:
js
if ( subtract && ( matches = rcssNum.exec( value ) ) &&
( matches[ 3 ] || "px" ) !== "px" ) {
elem.style[ dimension ] = value;
value = jQuery.css( elem, dimension );
}
matches[3] || "px":rcssNum.exec( "50" ) 的 matches[3] 是 ""(空串,因为 ([a-z%]*) 可以匹配空),"" || "px" → "px"。所以裸数字 50 被当作 px 处理,跳过换算。
而 "50%" 的 matches[3] 是 "%",不等于 "px" → 先写 style.width = "50%",再读 jQuery.css( elem, "width" ) 得到 "120px"(计算值),后续减法在 120 上进行。
这会造成一次额外的 reflow,但只在"非 px 值 + 需要盒模型换算"时才发生。
内建钩子系列二:margin / padding / borderWidth(6303--6328)
js
// These hooks are used by animate to expand properties
jQuery.each( {
margin: "",
padding: "",
border: "Width"
}, function( prefix, suffix ) {
jQuery.cssHooks[ prefix + suffix ] = {
expand: function( value ) {
var i = 0,
expanded = {},
// Assumes a single number if not a string
parts = typeof value === "string" ? value.split( " " ) : [ value ];
for ( ; i < 4; i++ ) {
expanded[ prefix + cssExpand[ i ] + suffix ] =
parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
}
return expanded;
}
};
if ( prefix !== "margin" ) {
jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
}
} );
这个 jQuery.each 的键是 prefix,值是 suffix:
| prefix | suffix | 生成的钩子名 |
|---|---|---|
"margin" |
"" |
cssHooks.margin |
"padding" |
"" |
cssHooks.padding |
"border" |
"Width" |
cssHooks.borderWidth |
expand 的语义:把 CSS 简写值展开成四个方向的具体属性。
CSS 的 margin/padding 简写遵循"上右下左"顺时针规则,且有省略规则:
| 简写 | 展开结果 |
|---|---|
10px |
top=10, right=10, bottom=10, left=10 |
10px 20px |
top=10, right=20, bottom=10, left=20 |
10px 20px 30px |
top=10, right=20, bottom=30, left=20 |
10px 20px 30px 40px |
top=10, right=20, bottom=30, left=40 |
expand 用一行实现了全部四条规则:
js
parts[ i ] || parts[ i - 2 ] || parts[ 0 ]
逐位分析(设 parts = ["10px","20px","30px"]):
i |
cssExpand[i] |
parts[i] |
parts[i-2] |
parts[0] |
结果 |
|---|---|---|---|---|---|
| 0 | "Top" |
"10px" |
--- | --- | "10px" ✓ |
| 1 | "Right" |
"20px" |
--- | --- | "20px" ✓ |
| 2 | "Bottom" |
"30px" |
parts[0]="10px" |
--- | "30px" ✓ |
| 3 | "Left" |
undefined |
parts[1]="20px" |
--- | "20px" ✓ |
完美复现"三位简写时 left 取 right"的规则。设 parts = ["10px","20px"]:
i |
parts[i] |
parts[i-2] |
parts[0] |
结果 |
|---|---|---|---|---|
| 0 | "10px" |
--- | --- | "10px" ✓ |
| 1 | "20px" |
--- | --- | "20px" ✓ |
| 2 | undefined |
parts[0]="10px" |
--- | "10px" ✓ |
| 3 | undefined |
parts[1]="20px" |
--- | "20px" ✓ |
设 parts = ["10px"]:
i |
结果 |
|---|---|
| 0 | "10px" |
| 1 | `undefined |
| 2 | `undefined |
| 3 | `undefined |
parts[-1] 在 JS 数组里是 undefined(数组没有负索引),所以安全。
为什么 border 的 suffix 是 "Width"? 因为 border: 1px solid red 这种简写包含宽度、样式、颜色三部分,expand 无法拆。jQuery 只支持 borderWidth: 5 展开成 borderTopWidth/Right/Bottom/Left。所以用 border + "Width" 拼出 borderWidth,而展开出的属性名是 border + Top + Width = borderTopWidth。✓
为什么 margin 不设 set?
js
if ( prefix !== "margin" ) {
jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
}
因为 margin 可以是负值 (margin-top: -10px),而 setPositiveNumber 会 Math.max( 0, ... ) 把负数钳到 0。padding 和 borderWidth 不能为负,所以可以用 setPositiveNumber 做保护。
这是一个非常精确的设计决策:同一个钩子协议,按属性的语义约束选择是否安装 set。
那 margin 走什么路径?没有 set 钩子 → jQuery.style 直接写 style.margin = value。所以 .css( "margin", -10 ) 会写 "-10"(注意:isAutoPx("margin") 为 true,所以补 px 得到 "-10px")✓。
内建钩子系列三:无
只有以上两类。opacity、zIndex 等都没有钩子------它们走 curCSS / style[name] 的通用路径。
编写一个自定义 cssHook:完整示例
假设我们要支持读取 CSS transform 的 translateX 分量:
js
$.cssHooks.translateX = {
get: function( elem, computed, extra ) {
// 只在计算模式下工作
if ( !computed ) {
return undefined; // 让 jQuery.style 读 elem.style.transform(其实读不到,见下)
}
var t = $.css( elem, "transform" );
if ( t && t !== "none" ) {
var m = new DOMMatrixReadOnly( t );
return extra === true ? m.m41 : m.m41 + "px";
}
return extra === true ? 0 : "0px";
},
set: function( elem, value ) {
// 自己写完,返回 undefined 阻止再写
var t = $.css( elem, "transform" );
var m = t && t !== "none" ? new DOMMatrixReadOnly( t ) : new DOMMatrixReadOnly();
m.m41 = parseFloat( value ) || 0;
elem.style.transform = m.toString();
return undefined;
}
};
几个要点:
get里判断computed,非计算模式返回undefined------遵循协议。set返回undefined------我们已经自己写了elem.style.transform,如果返回值,jQuery 会再去写elem.style[ "translateX" ](不存在的属性,无效)。- 注意
finalPropName的干扰 :我们的键是translateX,finalPropName( "translateX" )会检查"translateX" in emptyStyle→false;再vendorPropName→"msTranslateX"/"MozTranslateX"/"WebkitTranslateX"都不在 → 返回"translateX"。所以name保持translateX,cssHooks[ "translateX" ]命中 ✓。 - 读的时候 :
jQuery.css( elem, "translateX" )→hooks.get( elem, true, undefined )→ 返回"100px"。
一个坑:如果同时注册了 get 和 set,但用户调用 $( el ).css( "translateX", "50px" ),走 jQuery.style,hooks.set 被调用并自己写 transform,返回 undefined → jQuery.style 不再写 ✓。
钩子与 access 的交互
jQuery.fn.css(10.5)用 access 处理参数。当传入对象时,access 会遍历对象的键,对每个键调用 jQuery.style( elem, key, value )。所以批量设置时每个属性都会过一遍钩子。这是正确的行为。
10.2 getWidthOrHeight:尺寸的六条路径
js
function getWidthOrHeight( elem, dimension, extra ) {
// Start with computed style
var styles = getStyles( elem ),
// To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322).
// Fake content-box until we know it's needed to know the true value.
boxSizingNeeded = isIE || extra,
isBorderBox = boxSizingNeeded &&
jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
valueIsBorderBox = isBorderBox,
val = curCSS( elem, dimension, styles ),
offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
// Return a confounding non-pixel value or feign ignorance, as appropriate.
if ( rnumnonpx.test( val ) ) {
if ( !extra ) {
return val;
}
val = "auto";
}
if (
(
// Fall back to offsetWidth/offsetHeight when value is "auto"
// This happens for inline elements with no explicit setting (gh-3571)
val === "auto" ||
// Support: IE 9 - 11+
// Use offsetWidth/offsetHeight for when box sizing is unreliable.
// In those cases, the computed value can be trusted to be border-box.
( isIE && isBorderBox ) ||
( !support.reliableColDimensions() && nodeName( elem, "col" ) ) ||
( !support.reliableTrDimensions() && nodeName( elem, "tr" ) )
) &&
// Make sure the element is visible & connected
elem.getClientRects().length ) {
isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
// Where available, offsetWidth/offsetHeight approximate border box dimensions.
// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
// retrieved value as a content box dimension.
valueIsBorderBox = offsetProp in elem;
if ( valueIsBorderBox ) {
val = elem[ offsetProp ];
}
}
// Normalize "" and auto
val = parseFloat( val ) || 0;
// Adjust for the element's box model
return ( val +
boxModelAdjustment(
elem,
dimension,
extra || ( isBorderBox ? "border" : "content" ),
valueIsBorderBox,
styles,
// Provide the current computed size to request scroll gutter calculation (gh-3589)
val
)
) + "px";
}
(6061--6132)
这是本章最核心的函数 。.width()、.innerWidth()、.outerWidth()、.outerWidth( true ) 以及它们的高/宽变体,全部收敛到这里。我们把它的执行路径完整枚举。
阶段一:初始化(6064--6074)
boxSizingNeeded = isIE || extra(gh-4322)
这是一个性能优化:jQuery.css( elem, "boxSizing", false, styles ) 会读计算样式。虽然我们传了 styles(复用 getComputedStyle 结果),但读取 boxSizing 属性本身可能触发 style recalc。
什么时候真的需要知道 box-sizing?只有两种情形:
- IE:IE 的计算值在某些 box-sizing 情况下不可靠,必须知道
extra非空:需要做盒模型加减,必须知道起点是 content-box 还是 border-box
如果 extra 为空(用户只是 .css( "width" ) 要原始计算值),不需要知道 box-sizing,跳过读取。
"Fake content-box until we know it's needed"------isBorderBox 在这种情况下是 false,即"假装是 content-box"。这个假值只在最后一行 extra || ( isBorderBox ? "border" : "content" ) 里用到,而那时 extra 为空,所以传给 boxModelAdjustment 的 box 是 "content",isBorderBox 是 false → box === ( isBorderBox ? "border" : "content" ) → "content" === "content" → 返回 0,不做任何调整 ✓。
这就是"假装"的全部意义 :用一个便宜的假值,让后续代码走"不调整"的分支,从而完全避免读取 boxSizing。
offsetProp 的构造(6074):
js
offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 );
dimension 是 "width" 或 "height"。"width"[0].toUpperCase() = "W",.slice(1) = "idth" → "offsetWidth" ✓。
阶段二:非 px 值的处理(6076--6082)
js
// Return a confounding non-pixel value or feign ignorance, as appropriate.
if ( rnumnonpx.test( val ) ) {
if ( !extra ) {
return val;
}
val = "auto";
}
rnumnonpx = /^(pnum)(?!px)[a-z%]+$/i(9.1 讲过)。它匹配"数字 + 非 px 单位"。
当计算出的 val 是 "50%"、"2em"、"10vh" 时:
!extra(用户要原始值) → 直接返回"50%"。这是为什么要这个分支:$( el ).css( "width" )应该返回"50%"而不是换算后的 px。注释说 "Return a confounding non-pixel value"(返回一个恼人的非像素值)------jQuery 承认这是"令人困惑的",但这是 CSS 的真相。- 有
extra(要数字或盒模型换算) → 设为"auto",强制进入下一阶段的 offset 回退。因为非 px 值无法参与盒模型加减(你不知道 50% 是多少 px)。
这个 return val 是 getWidthOrHeight 唯一的提前返回 ,而且它返回的是字符串,不是数字。所以:
js
$( el ).css( "width" ); // "50%" ← 字符串,因为 extra 为 undefined
$( el ).width(); // 数字,因为 extra 为 "border",走 offset 回退
这两个值不一致! .css("width") 给 "50%",.width() 给实际像素数。这是 jQuery 的一个已知行为,也是很多人的困惑源。我们在 10.10 会列为陷阱。
阶段三:offset 回退的四条件(6085--6114)
js
if (
(
val === "auto" ||
( isIE && isBorderBox ) ||
( !support.reliableColDimensions() && nodeName( elem, "col" ) ) ||
( !support.reliableTrDimensions() && nodeName( elem, "tr" ) )
) &&
elem.getClientRects().length ) {
...
}
四个"或"条件 + 一个"与"条件:
| 条件 | 触发场景 | 为什么 |
|---|---|---|
val === "auto" |
内联元素没有显式宽度(gh-3571) | getComputedStyle 对 display:inline 且无显式 width 的元素报 "auto" |
isIE && isBorderBox |
IE 的 border-box 计算值不可靠 | IE 9--11 在 border-box 下计算值有误,offset* 才对 |
!reliableColDimensions() && nodeName(elem,"col") |
Firefox 135+ / Safari 18.3+ 的 <col> |
9.5 讲过,计算宽度恒为 0 或按 span=1 算 |
!reliableTrDimensions() && nodeName(elem,"tr") |
Firefox 70--135 / IE 10--11 的 <tr> |
9.5 讲过,计算高度含边框与否不一致 |
elem.getClientRects().length :确保元素可见且已连接。这是 9.3 里出现过的同一个技巧------getClientRects() 对隐藏/游离元素返回空数组,比 offsetWidth === 0 更可靠(因为 offsetWidth 为 0 也可能是真的 0 宽度元素)。
回退内部(6105--6113):
js
isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
// Where available, offsetWidth/offsetHeight approximate border box dimensions.
// Where not available (e.g., SVG), assume unreliable box-sizing and interpret the
// retrieved value as a content box dimension.
valueIsBorderBox = offsetProp in elem;
if ( valueIsBorderBox ) {
val = elem[ offsetProp ];
}
- 重新读
boxSizing(无条件)。因为进入回退说明我们要用offsetWidth,必须确切知道它的盒模型语义。 valueIsBorderBox = offsetProp in elem:检查offsetWidth是否存在。SVG 元素没有offsetWidth(SVGElement 不继承 HTMLElement 的布局属性)!所以对 SVG 元素,valueIsBorderBox为false,val保持原计算值,且被当作 content-box 解释。val = elem[ offsetProp ]:offsetWidth是 border-box 的整数(含 content + padding + border,不含 margin,含滚动条)。
注意 isBorderBox 和 valueIsBorderBox 是两个不同的变量:
| 变量 | 含义 | 用于 |
|---|---|---|
isBorderBox |
元素的 box-sizing 计算值 |
传给 boxModelAdjustment 作为 isBorderBox 参数 |
valueIsBorderBox |
当前 val 这个数字代表的是哪种盒 |
传给 boxModelAdjustment 作为 isBorderBox 参数... |
等等,看最后调用:
js
boxModelAdjustment(
elem,
dimension,
extra || ( isBorderBox ? "border" : "content" ), // box
valueIsBorderBox, // isBorderBox 参数
styles,
val // computedVal
)
传给 boxModelAdjustment 的是 valueIsBorderBox ,不是 isBorderBox。
box=extra || ( isBorderBox ? "border" : "content" ):目标盒。若无extra,目标是"元素当前 box-sizing 对应的默认盒"isBorderBox(第 4 参)=valueIsBorderBox:val 这个数字是按哪种盒解释的
这个区分是 getWidthOrHeight 最容易看错的地方。isBorderBox 变量其实只用于构造 box 参数(当 extra 为空时) ,而真正决定加减方向的是 valueIsBorderBox。
举例:box-sizing: border-box 的元素,val 来自 curCSS(未走 offset 回退),则 isBorderBox = true,valueIsBorderBox = true。box = extra || "border"。若 extra = "content",boxModelAdjustment 的 box === "content",isBorderBox === true → 走 else 分支,减去 padding 和 border ✓。
若走了 offset 回退,且元素有 offsetWidth(HTML 元素),则 valueIsBorderBox = true(offsetWidth 恒是 border-box),无论 boxSizing 是什么。这是正确的:offsetWidth 按定义就是 border box。
阶段四:归一化与盒模型调整(6116--6131)
js
// Normalize "" and auto
val = parseFloat( val ) || 0;
// Adjust for the element's box model
return ( val +
boxModelAdjustment( elem, dimension, extra || ( isBorderBox ? "border" : "content" ),
valueIsBorderBox, styles, val )
) + "px";
parseFloat( val ) || 0:把 "100px" 变 100,把 ""/"auto"/NaN 变 0。
最后返回 ( val + 调整量 ) + "px" ------注意末尾拼了 "px"。所以 getWidthOrHeight 总是返回字符串,即使调用方要的是数字。
那 .width() 怎么返回数字?因为 cssHooks.width.get 的返回值被 jQuery.css 处理:
js
if ( extra === "" || extra ) {
num = parseFloat( val );
return extra === true || isFinite( num ) ? num || 0 : val;
}
jQuery.css 会 parseFloat 一次,把 "142px" 变成 142 ✓。
所以字符串 → 数字 的转换发生在 jQuery.css 层,不是 getWidthOrHeight 层 。这是一个清晰的分工:getWidthOrHeight 负责"算出正确的 px 字符串",jQuery.css 负责"按 extra 决定返回类型"。
六条完整路径推演
设一个 box-sizing: content-box 的 div,CSS 为 width: 100px; padding: 10px; border: 2px solid; margin: 5px。计算值 width = "100px",offsetWidth = 100 + 20 + 4 = 124。
| # | extra |
val 初值 |
阶段二 | 阶段三 | val 终值 |
box |
valueIsBorderBox |
boxModelAdjustment |
返回 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | undefined |
"100px" |
rnumnonpx 不匹配 |
不触发 | 100 |
"content" |
false |
0(早退) |
"100px" |
| 2 | "" |
"100px" |
同上 | 不触发 | 100 |
"content" |
false |
0 |
"100px" |
| 3 | "padding" |
"100px" |
不匹配 | 不触发 | 100 |
"padding" |
false |
+20(上下/左右 padding) |
"120px" |
| 4 | "border" |
"100px" |
不匹配 | 不触发 | 100 |
"border" |
false |
+20+4 = +24 |
"124px" |
| 5 | "margin" |
"100px" |
不匹配 | 不触发 | 100 |
"margin" |
false |
+24 + 10 = +34 |
"134px" |
| 6 | true |
"100px" |
不匹配 | 不触发 | 100 |
true |
false |
box=true 不等于 content/border → 走 else... | 见下 |
第 6 行需要单独说明:extra === true 时,box 参数是 true(因为 extra || 短路)。在 boxModelAdjustment 里:
js
if ( box === ( isBorderBox ? "border" : "content" ) ) {
return 0;
}
isBorderBox 为 false → 右边是 "content";box 是 true → true === "content" 为 false → 不早退。
然后 for 循环里:
box === "margin"?true === "margin"→ false!isBorderBox→ true →delta += padding;box !== "padding"(true !== "padding")→ true →delta += border
所以 extra === true 等价于 extra === "border"。这就是为什么 .width() 和 .outerWidth() 的区别靠 extra 是 "border" 还是 true(见 10.6)。
再看一个 border-box 的例子。设 box-sizing: border-box,width: 100px; padding: 10px; border: 2px。
此时计算值 width 是 "100px"(border-box 下 width 指定的是 border box 总宽),offsetWidth = 100。
| # | extra |
box |
valueIsBorderBox |
调整 | 返回 |
|---|---|---|---|---|---|
| 1 | undefined |
"border"(isBorderBox 为 true) |
true |
box === "border" → 早退 0 |
"100px" |
| 2 | "content" |
"content" |
true |
-20 -4 = -24 |
"76px" |
| 3 | "padding" |
"padding" |
true |
-4(只减 border) |
"96px" |
| 4 | "border" |
"border" |
true |
0(早退) |
"100px" |
| 5 | "margin" |
"margin" |
true |
0 + marginDelta(10) = +10 |
"110px" |
第 3 行验证:box === "padding",isBorderBox === true → 走 else 分支:
box === "content"?否 → 不减 paddingbox !== "margin"?"padding" !== "margin"→ 是 →delta -= border(-4)
结果 100 - 4 = 96 ✓(padding box = border box - border)。
第 5 行验证:box === "margin",走 else:
box === "content"?否box !== "margin"?否 → 不减 border- 循环外
marginDelta += margin(+10) - 但循环内的
if ( box === "margin" ) marginDelta += ...在每次迭代都执行,i取 1 和 3(width)→marginRight + marginLeft= 10 delta为 0(因为都没执行)!isBorderBox && computedVal >= 0?isBorderBox为 true → 跳过滚动条计算- 返回
0 + 10 = 10 100 + 10 = 110✓
滚动条 gutter 的处理(gh-3589)
js
// 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(
elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] -
computedVal -
delta -
extra -
0.5
// If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter
// Use an explicit zero to avoid NaN (gh-3964)
) ) || 0;
}
(6041--6056)
问题 :当一个内容溢出产生滚动条的元素,我们要算它的 content-box 宽度时,getComputedStyle().width 报的是不含滚动条的内容宽度,而 CSS 意义上的 content box 也不含滚动条(滚动条在 padding box 内、content box 外)。
但 offsetWidth 是 content + padding + scrollbar + border(取整)。所以:
css
offsetWidth = contentWidth + padding*2 + scrollGutter + border*2
我们要的 delta(从 content 到目标盒的增量)里,漏掉了 scrollGutter。
公式推导:
ini
scrollGutter = offsetWidth - computedVal - delta - extra - 0.5
其中:
computedVal= 传入的 val(content 宽度的浮点值)delta= 已累加的 padding/border(本轮)extra= 在box === "padding"分支里单独累计的 border 宽度(见 6022)-0.5配合Math.ceil实现"四舍五入到最近的整数"
等等,Math.ceil( x - 0.5 ) 是 Math.round( x ) 的等价形式(对于正数)。jQuery 用这个形式可能只是为了明确"取整"意图。
Math.max( 0, ... ) 保证不会出现负的 gutter(浮点误差导致)。
|| 0 的作用 (gh-3964):如果 elem.offsetWidth 是 undefined(SVG 元素),整个算式是 NaN,Math.ceil( NaN ) 是 NaN,Math.max( 0, NaN ) 是 NaN,NaN || 0 → 0 ✓。
computedVal >= 0 条件 :只有当传入的 val 非负时才算。如果 val 是 0(隐藏元素),跳过。注意这里 computedVal 参数在 boxModelAdjustment 的签名里是第 6 个参数,可选 ------cssHooks.width.set(6280)调用时只传了 5 个参数,所以 computedVal 是 undefined,undefined >= 0 是 false → 跳过滚动条计算 ✓。
这个设计很巧妙 :boxModelAdjustment 有两种调用模式:
- 读模式 (
getWidthOrHeight):传 6 个参数,计算滚动条 - 写模式 (
cssHooks.width.set):传 5 个参数,不算滚动条(因为设置宽度时不应该考虑当前滚动条)
getClientRects() vs offsetWidth 的判据差异
源码里两处用了不同的可见性判据:
| 位置 | 判据 | 用途 |
|---|---|---|
| 6103 | elem.getClientRects().length |
决定是否可以用 offset 回退 |
6077 之前的 curCSS |
isAttached( elem ) |
决定是否回退到内联样式 |
jQuery.fn.offset(9372) |
elem.getClientRects().length |
决定是否返回 {0,0} |
getClientRects() 返回一个 DOMRectList,包含元素的所有边界矩形(内联元素换行会产生多个矩形)。空数组意味着元素没有渲染(display:none、游离、或本身是空的内联元素)。
相比 offsetWidth === 0,getClientRects().length 的好处:
- 对
display: none一定返回空(而offsetWidth也是 0,两者一致) - 对 SVG 元素有效(
offsetWidth不存在) - 能区分"元素不可见"和"元素宽度为 0" ------一个
width: 0的可见元素,getClientRects()返回一个 width 为 0 的矩形(长度 1),而offsetWidth是 0
第 3 点是关键。jQuery 需要的是"这个元素有没有被渲染",而不是"它有多宽"。
10.3 boxModelAdjustment:四盒模型的加减法
js
function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) {
var i = dimension === "width" ? 1 : 0,
extra = 0,
delta = 0,
marginDelta = 0;
// Adjustment may not be necessary
if ( box === ( isBorderBox ? "border" : "content" ) ) {
return 0;
}
for ( ; i < 4; i += 2 ) {
...
}
// Account for positive content-box scroll gutter ...
if ( !isBorderBox && computedVal >= 0 ) { ... }
return delta + marginDelta;
}
(5990--6059)
var i = dimension === "width" ? 1 : 0 与 i += 2
js
var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
对 width ,我们只需要 Right + Left → i 从 1 开始,步长 2 → i = 1, 3 → cssExpand[1] = "Right"、cssExpand[3] = "Left" ✓
对 height ,我们只需要 Top + Bottom → i 从 0 开始,步长 2 → i = 0, 2 → "Top"、"Bottom" ✓
这是一个**用数组索引 + 步长表达"两个方向"**的技巧,避免了两个独立的循环或方向数组。代价是可读性,收益是代码量减半。
早退条件(5997--5999)
js
// Adjustment may not be necessary
if ( box === ( isBorderBox ? "border" : "content" ) ) {
return 0;
}
语义:如果目标盒就是"当前 val 所代表的盒",不需要调整。
isBorderBox === true→ val 已经是 border box → 目标是"border"时不调isBorderBox === false→ val 是 content box → 目标是"content"时不调
这是整个函数最快的路径,也是最常见的路径(.css("width") 无 extra 时)。
循环体的两条大分支
js
for ( ; i < 4; i += 2 ) {
// Both box models exclude margin
// Count margin delta separately to only add it after scroll gutter adjustment.
// This is needed to make negative margins work with `outerHeight( true )` (gh-3982).
if ( box === "margin" ) {
marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
}
// If we get here with a content-box, we're seeking "padding" or "border" or "margin"
if ( !isBorderBox ) {
// Add padding
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 {
// For "content", subtract padding
if ( box === "content" ) {
delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
}
// For "content" or "padding", subtract border
if ( box !== "margin" ) {
delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
}
}
我们把它整理成一张完整的决策表。设 P = padding(单侧),B = border(单侧),M = margin(单侧)。
content-box 起点(isBorderBox === false):
box |
padding 处理 | border 处理 | margin 处理 | delta(双侧) |
marginDelta |
总调整 |
|---|---|---|---|---|---|---|
"content" |
--- | --- | --- | --- | --- | 0(早退) |
"padding" |
+P |
记入 extra(不进 delta) |
--- | +2P |
0 |
+2P + scrollGutter |
"border" |
+P |
+B |
--- | +2P+2B |
0 |
+2P+2B + gutter |
"margin" |
+P |
+B |
+M 进 marginDelta |
+2P+2B |
+2M |
+2P+2B+2M |
true |
+P |
+B |
--- | +2P+2B |
0 |
同 "border" |
border-box 起点(isBorderBox === true):
box |
padding 处理 | border 处理 | margin 处理 | delta(双侧) |
marginDelta |
总调整 |
|---|---|---|---|---|---|---|
"border" |
--- | --- | --- | --- | --- | 0(早退) |
"padding" |
不减 | -B |
--- | -2B |
0 |
-2B |
"content" |
-P |
-B |
--- | -2P-2B |
0 |
-2P-2B |
"margin" |
不减 | 不减(box !== "margin" 为 false) |
+M |
0 |
+2M |
+2M |
true |
不减 | -B |
--- | -2B |
0 |
-2B |
验证 "margin" 在 border-box 下的行为:目标是 margin box,起点是 border box,差值就是 2M ✓。代码里 box !== "margin" 为 false 所以不减 border ✓,box === "content" 为 false 所以不减 padding ✓。正确。
为什么 margin 要单独累计(gh-3982)
注释解释得很清楚:
Count margin delta separately to only add it after scroll gutter adjustment. This is needed to make negative margins work with
outerHeight( true ).
滚动条 gutter 的计算公式:
ini
gutter = offsetWidth - computedVal - delta - extra - 0.5
offsetWidth 是 border box (不含 margin)。所以公式里不能 包含 margin。如果 delta 里混入了 margin,gutter 就会算错(变成负数)。
而 marginDelta 在 return delta + marginDelta 时才加上,在 gutter 计算之后。
负 margin 的问题 (gh-3982):如果 margin 是 -10px,而 gutter 计算出的 Math.max( 0, ... ) 把它钳到 0,就会导致 outerWidth( true ) 在负 margin 时不准。分开累计后,delta 里只有非负的 padding/border,Math.max( 0, ... ) 不会误伤负 margin ✓。
extra 变量的作用
在 box === "padding" 且 !isBorderBox 的分支里,border 宽度不进 delta ,而是进 extra:
js
} else {
extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
}
为什么?因为目标是 padding box,不应该加 border。但 gutter 公式需要知道 border 宽度:
css
offsetWidth = content + padding + gutter + border
gutter = offsetWidth - content - padding - border = offsetWidth - computedVal - delta - extra
此时 delta = 2P,extra = 2B → gutter = offsetWidth - computedVal - 2P - 2B ✓
所以 extra 是一个"参与公式但不参与结果"的中间量。这是为了复用同一个公式处理不同的目标盒。
10.4 setPositiveNumber 与负边距
js
function setPositiveNumber( _elem, value, subtract ) {
// Any relative (+/-) values have already been
// normalized at this point
var matches = rcssNum.exec( value );
return matches ?
// Guard against undefined "subtract", e.g., when used as in cssHooks
Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) :
value;
}
(5978--5988)
两种身份
setPositiveNumber 有两个调用身份:
- 作为
cssHooks.padding.set/cssHooks.borderWidth.set(6326):jQuery.style调用hooks.set( elem, value, extra ),返回值被写回 style。 - 作为
cssHooks.width.set的最后一步 (6297):return setPositiveNumber( elem, value, subtract )。
注意身份 1 的 extra 参数在这里变成了 subtract?不对------签名是 ( _elem, value, subtract ),jQuery.style 调用 hooks.set( elem, value, extra )。所以第 3 个参数既可能是"盒模型 extra 字符串",也可能是"要减去的像素数"。
在身份 1 里,extra 通常是 undefined(用户直接 .css( "padding", 10 )),所以 subtract || 0 → 0。
在身份 2 里,subtract 是 boxModelAdjustment 的结果(数字或 0)。
第一个参数为什么叫 _elem
下划线前缀表示"未使用"(jQuery 代码规范里用 _ 前缀标记刻意忽略的参数)。setPositiveNumber 不需要 elem------它只做数值运算。
但为什么要保留这个参数?因为作为钩子方法,它必须匹配 ( elem, value, extra ) 的协议签名。这是一个签名兼容导致的空参数。
Math.max( 0, ... ) 的语义
"Positive" 体现在这里:结果永不为负。
场景:元素 box-sizing: border-box,padding: 20px,用户执行 .width( 10 )。
extra = "border"(默认)boxModelAdjustment( ..., "border", isBorderBox=true, ... )→box === "border"→ 早退0?
等等,这不对。让我重新看 cssHooks.width.set:
js
subtract = extra ?
boxModelAdjustment( elem, dimension, extra, isBorderBox, styles ) : 0;
这里 extra 是从 jQuery.style 传下来的。.width( 10 ) 时,jQuery.fn.width 调用 jQuery.style( elem, type, value, extra ),extra 是 "border"(见 10.6)。
所以 boxModelAdjustment( elem, "width", "border", isBorderBox, styles ):
box === "border",isBorderBox === true→box === ( true ? "border" : "content" )→"border" === "border"→ 早退 0
subtract = 0。所以 .width( 10 ) 在 border-box 下就是写 10px ✓(因为 border-box 的 width 就是 border box 宽度,用户要的就是这个)。
再看 content-box 的情况:box-sizing: content-box,padding: 20px,.width( 100 ):
extra = "border"(jQuery 的.width()默认按 border?不对)
让我重新看 10.6 的代码(9531):
js
}, type, chainable ? margin : undefined, chainable );
access( this, fn, type, value, chainable )。access 的第 4 个参数是 value,第 5 个是 chainable。所以传给 fn 的 value 参数是 margin(用户传入的第一个参数)。
而 jQuery.fn.width 的签名是 function( margin, value ):
js
jQuery.fn[ funcName ] = function( margin, value ) {
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
return access( this, function( elem, type, value ) {
...
return value === undefined ?
jQuery.css( elem, type, extra ) :
jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable );
};
所以:
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" )- 对
width:defaultExtra = "content"(type),所以extra = "content" - 对
innerWidth:defaultExtra = "padding" - 对
outerWidth:defaultExtra = ""(空串!)→"" || (...)→margin === true ? "margin" : "border"
- 对
注意 defaultExtra 为 "" 是 falsy,所以 outerWidth 走后半段:margin === true ? "margin" : "border" ✓
那么对 content-box 元素 .width( 100 ):
extra = "content"boxModelAdjustment( elem, "width", "content", isBorderBox=false, styles )→box === "content"→ 早退0subtract = 0setPositiveNumber( elem, 100, 0 )→Math.max( 0, 100 - 0 ) + "px"→"100px"✓
对 content-box 元素 .outerWidth( 100 ):
extra = "border"boxModelAdjustment( elem, "width", "border", false, styles )→delta = 2P + 2Bsubtract = 2P + 2B(比如 48)setPositiveNumber( elem, 100, 48 )→"52px"✓
完美:用户要 outerWidth 100,jQuery 写 width: 52px(content),加上 48 的 padding+border 正好 100 ✓
负值的情形 :如果 subtract = 120 而用户要 100,Math.max( 0, 100 - 120 ) → 0。所以 .outerWidth( 100 ) 在一个 padding+border 超过 100 的元素上,会把 content width 设为 0,而不是负数。这是 CSS 的约束(width 不能为负)与 jQuery 的防护。
matches[ 3 ] || "px"
如果 value 是 "50%":matches = [ "50%", undefined, "50", "%" ] → Math.max( 0, 50 - subtract ) + "%"。
但前面 cssHooks.width.set 已经把非 px 值换算成了 px(6290--6295),所以到这里 value 通常是 px 字符串或数字。
如果 value 是数字 100:rcssNum.exec( 100 ) ------ 正则的 exec 会把参数转成字符串 "100" → matches[2] = "100",matches[3] = "" → "" || "px" → "px" ✓
如果 value 是 "auto" 或 "inherit":rcssNum 不匹配 → matches 为 null → 返回 value 原样 。所以 .width( "auto" ) 会写 style.width = "auto" ✓(这是有效的 CSS)。
10.5 jQuery.fn.css 与 access 的第四态
js
jQuery.fn.extend( {
css: function( name, value ) {
return access( this, function( elem, name, value ) {
var styles, len,
map = {},
i = 0;
if ( Array.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
}
return map;
}
return value !== undefined ?
jQuery.style( elem, name, value ) :
jQuery.css( elem, name );
}, name, value, arguments.length > 1 );
}
} );
(6330--6353)
access 的四态回顾
access 是 jQuery 的通用读写访问器 (第 03 章讲过)。jQuery.fn.css 展示了它的完整四态:
js
access( this, fn, name, value, chainable )
| 调用形式 | name |
value |
arguments.length > 1 |
access 行为 |
|---|---|---|---|---|
.css( "color" ) |
"color" |
undefined |
false |
读模式:只处理 this[0],返回 fn 的返回值 |
.css( "color", "red" ) |
"color" |
"red" |
true |
写模式:遍历所有元素,返回 this(链式) |
.css( { color: "red" } ) |
{...} |
undefined |
false |
对象读? 不------access 检测 typeof name === "object" → 遍历键,逐个写,返回 this |
.css( [ "a", "b" ] ) |
[...] |
undefined |
false |
Array.isArray 分支在 fn 内部处理 |
关键在 access 内部对 typeof name === "object" 的判断:
js
// access 源码(第 03 章)
if ( typeof name === "object" ) {
for ( ; i < length; i++ ) {
fn( elems[ i ], name ); // 注意:只传 2 个参数,value 是 undefined
}
return chainable ? elems : ...;
}
但我们的 fn 里检查的是 value !== undefined,而对象模式下 value 是 undefined... 这看起来会走读分支?
不对。让我重新看 access。实际上 jQuery 的 access 在对象分支里是:
js
if ( typeof name === "object" ) {
for ( ; i < length; i++ ) {
// 对每个元素,遍历对象的键
for ( key in name ) {
fn( elems[ i ], key, name[ key ] );
}
}
}
即把对象的每个键值对拆开 传给 fn。这才是 access 的完整逻辑(第 03 章有详细讲解)。所以 .css( { color: "red" } ) 会让 fn 被调用两次:fn( el, "color", "red" )。
于是 value !== undefined → jQuery.style( elem, "color", "red" ) ✓
arguments.length > 1 作为 chainable:
js
}, name, value, arguments.length > 1 );
.css( "color" )→arguments.length === 1→chainable = false→ 读模式 ✓.css( "color", "red" )→arguments.length === 2→chainable = true→ 写模式 ✓.css( { color: "red" } )→arguments.length === 1→chainable = false??
这不对------对象模式应该返回 this 以支持链式。
实际上 access 在 typeof name === "object" 分支里忽略 chainable,总是返回 elems 。让我回忆第 03 章的内容:access 的结构是:
js
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
len = elems.length,
bulk = key == null;
// Sets many values
if ( typeof key === "object" ) {
chainable = true;
for ( i in key ) {
access( elems, fn, i, key[ i ], true, emptyGet, raw );
}
// Sets one value
} else if ( value !== undefined ) {
chainable = true;
...
}
return chainable ? elems : ...;
};
是的!access 在对象分支里强制 chainable = true ,然后递归调用自己处理每个键值对。所以 .css( { color: "red" } ) 返回 this ✓
而且这也是为什么 .css( "color", null )(value 是 null,不是 undefined)会走"设置"分支并 chainable = true------但 jQuery.style 里 value == null 会 no-op。所以链式返回,但没效果。
数组读分支
js
if ( Array.isArray( name ) ) {
styles = getStyles( elem );
len = name.length;
for ( ; i < len; i++ ) {
map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
}
return map;
}
.css( [ "width", "height" ] ) 返回 { width: "100px", height: "50px" }。
注意 jQuery.css( elem, name[i], false, styles ) 的第三个参数是 false:
在 jQuery.css 里:
js
if ( extra === "" || extra ) { ... }
false 既不是 "" 也不是 truthy → 跳过数值化,返回原始字符串。
所以数组读返回的是字符串值 ,不是数字。这和普通读 .css( "width" )(extra 为 undefined)一致 ✓
但 styles 只取一次 :getStyles( elem ) 在循环外,循环中复用。这就是 9.2 讲的复用策略。
为什么 jQuery.fn.css 不支持 extra
.css() 没有 extra 参数,所以无法直接读"带盒模型的尺寸"。这就是为什么需要 .width() / .innerWidth() / .outerWidth() 三个独立方法------它们各自固定了 extra。
对比:
| API | 底层调用 | extra |
|---|---|---|
.css( "width" ) |
jQuery.css( elem, "width" ) |
undefined → 字符串 |
.width() |
jQuery.css( elem, "width", "content" ) |
"content" → 数字 |
.innerWidth() |
jQuery.css( elem, "width", "padding" ) |
"padding" → 数字 |
.outerWidth() |
jQuery.css( elem, "width", "border" ) |
"border" → 数字 |
.outerWidth( true ) |
jQuery.css( elem, "width", "margin" ) |
"margin" → 数字 |
一个容易忽略的陷阱:.css( name ) 对空集合
js
$( "#nonexistent" ).css( "color" );
access 在 bulk || !fn 之外的情形下,如果 elems.length === 0,会走 emptyGet 分支:
js
return chainable ? elems :
bulk ? fn.call( elems ) :
length ? fn( elems[ 0 ], key ) : emptyGet;
emptyGet 默认 undefined。所以 $( "#nope" ).css( "color" ) 返回 undefined (不是 "",也不是抛错)✓
而 $( "#nope" ).css( [ "a", "b" ] ) 同样返回 undefined(因为 access 在 length === 0 时根本不调 fn)。
10.6 尺寸方法族:height / innerHeight / outerHeight
js
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
jQuery.each( {
padding: "inner" + name,
content: type,
"": "outer" + name
}, function( defaultExtra, funcName ) {
// Margin is only for outerHeight, outerWidth
jQuery.fn[ funcName ] = function( margin, value ) {
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
return access( this, function( elem, type, value ) {
var doc;
if ( isWindow( elem ) ) {
// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
return funcName.indexOf( "outer" ) === 0 ?
elem[ "inner" + name ] :
elem.document.documentElement[ "client" + name ];
}
// Get document width or height
if ( elem.nodeType === 9 ) {
doc = elem.documentElement;
// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
// whichever is greatest
return Math.max(
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
elem.body[ "offset" + name ], doc[ "offset" + name ],
doc[ "client" + name ]
);
}
return value === undefined ?
// Get width or height on the element, requesting but not forcing parseFloat
jQuery.css( elem, type, extra ) :
// Set width or height on the element
jQuery.style( elem, type, value, extra );
}, type, chainable ? margin : undefined, chainable );
};
} );
} );
(9487--9534)
二重 jQuery.each 生成六个方法
外层 :{ Height: "height", Width: "width" }
name = "Height",type = "height"name = "Width",type = "width"
内层 :{ padding: "inner" + name, content: type, "": "outer" + name }
defaultExtra |
funcName(Height 轮) |
funcName(Width 轮) |
|---|---|---|
"padding" |
"innerHeight" |
"innerWidth" |
"content" |
"height" |
"width" |
"" |
"outerHeight" |
"outerWidth" |
注意 "" 作为对象键是合法的 (空字符串键)。这是 jQuery 故意的------因为 defaultExtra || 需要它 falsy。
这个二重循环的模式在第 08 章的 addClass/removeClass、第 07 章的 DOM 插入方法族里都出现过,是 jQuery 的标准代码生成手法:用数据驱动避免复制粘贴六份几乎相同的实现。
chainable 的计算
js
var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
chainable 决定 access 的第 5 个参数(是否链式返回)。
逐项分析:
arguments.length:$( el ).width()是 0 → falsy →chainable = 0(falsy)→ 读模式;$( el ).width( 100 )是 1 → truthy → 继续判断defaultExtra:对height/innerHeight是非空字符串 → truthy →chainable = true→ 写模式typeof margin !== "boolean":对outerHeight,defaultExtra是""(falsy),所以看这里
关键场景 :$( el ).outerHeight( true ) ------ margin 是 true(boolean)!此时:
arguments.length= 1 → truthydefaultExtra=""→ falsytypeof true !== "boolean"→ false- 所以
chainable = false→ 读模式!
这是对的:outerHeight( true ) 是"带 margin 地读",不是写。✓
而 $( el ).outerHeight( 100 ):
margin是100,typeof 100 !== "boolean"→ truechainable = true→ 写模式 ✓
$( el ).outerHeight( true, 100 )? 这不是合法用法。签名是 ( margin, value ),margin === true 表示"读时包含 margin",value 只在写时用。实际用法:
js
$( el ).outerHeight( true ); // 读,含 margin
$( el ).outerHeight( 100 ); // 写,按 border 盒换算
$( el ).outerHeight( 100, true ); // 写,按 margin 盒换算
第三个用法:margin = 100,value = true → extra = "" || ( 100 === true || true === true ? "margin" : "border" ) → true === true → "margin" ✓ 而 chainable = 1 && ( "" || typeof 100 !== "boolean" ) = 1 && ( "" || true ) = true ✓
extra 的计算
js
extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
| 方法 | defaultExtra |
extra 结果 |
|---|---|---|
.height() / .height( 100 ) |
"content" |
"content" |
.innerHeight() |
"padding" |
"padding" |
.outerHeight() |
"" |
`margin === true |
.outerHeight( true ) |
"" |
"margin" |
access 的调用
js
return access( this, function( elem, type, value ) { ... },
type, chainable ? margin : undefined, chainable );
- 第 3 参
key=type("width"/"height") - 第 4 参
value=chainable ? margin : undefined
这里有一个精妙之处 :写模式下 access 把 margin 当作 value 传给 fn。但 margin 可能是 true(boolean)!
.outerHeight( true )→chainable = false→ 第 4 参是undefined→ 读模式 ✓(不会把true当值写进去).outerHeight( 100 )→chainable = true→ 第 4 参是100✓.outerHeight( 100, true )→chainable = true→ 第 4 参是100✓(true在value里,用于算extra)
所以 chainable ? margin : undefined 是防止 true 被误当作 CSS 值的守卫。
fn 内的三个分支
分支 1:window
js
if ( isWindow( elem ) ) {
// $( window ).outerWidth/Height return w/h including scrollbars (gh-1729)
return funcName.indexOf( "outer" ) === 0 ?
elem[ "inner" + name ] :
elem.document.documentElement[ "client" + name ];
}
$( window ).width() → window.document.documentElement.clientWidth(视口宽度,不含 滚动条) $( window ).outerWidth() → window.innerWidth(含滚动条,gh-1729)
funcName.indexOf( "outer" ) === 0 判断方法名是否以 "outer" 开头。对 outerWidth/outerHeight 成立。
elem[ "inner" + name ] → window.innerHeight / window.innerWidth ✓ elem.document.documentElement[ "client" + name ] → documentElement.clientHeight / clientWidth ✓
注意 :$( window ).innerWidth() 走的是 else 分支(不以 outer 开头)→ 返回 clientWidth,即和 .width() 一样。这是 jQuery 的行为:innerWidth 对 window 没有独立语义。
分支 2:document(nodeType 9)
js
if ( elem.nodeType === 9 ) {
doc = elem.documentElement;
// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
// whichever is greatest
return Math.max(
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
elem.body[ "offset" + name ], doc[ "offset" + name ],
doc[ "client" + name ]
);
}
$( document ).height() 返回文档的总高度。为什么取 5 个值的最大值?
因为不同浏览器/不同情况下,这几个值哪个最准是不确定的:
| 属性 | 含义 | 何时最大 |
|---|---|---|
body.scrollHeight |
body 的内容高度(含溢出) | 内容溢出时 |
documentElement.scrollHeight |
html 的内容高度 | 通常 |
body.offsetHeight |
body 的 border-box 高度 | body 有 border/margin 时 |
documentElement.offsetHeight |
html 的 border-box 高度 | --- |
documentElement.clientHeight |
html 的视口高度(不含滚动条) | 内容不足一屏时 |
取 Math.max 是一种保守的启发式:保证返回值不小于真实内容高度。代价是可能偏大(比如 body 有 margin 时)。
这是 jQuery 里典型的"没有标准答案时的经验公式"。
分支 3:普通元素
js
return value === undefined ?
// Get width or height on the element, requesting but not forcing parseFloat
jQuery.css( elem, type, extra ) :
// Set width or height on the element
jQuery.style( elem, type, value, extra );
读:jQuery.css( elem, "width", "content" ) → 经 cssHooks.width.get → getWidthOrHeight → 数字 写:jQuery.style( elem, "width", 100, "content" ) → 经 cssHooks.width.set → setPositiveNumber
注意注释 "requesting but not forcing parseFloat"------extra 是字符串("content"),不是 true。所以 jQuery.css 里:
js
return extra === true || isFinite( num ) ? num || 0 : val;
extra === true 为 false,isFinite( num ) 决定。如果计算值是 "auto",num 是 NaN → 返回 val(字符串 "auto")!
等等,但 getWidthOrHeight 返回的是 "142px"(因为末尾拼了 "px"),parseFloat( "142px" ) 是 142,isFinite 为 true → 返回 142 ✓
如果 getWidthOrHeight 提前返回了非 px 值(阶段二的 return val),比如 "50%":parseFloat( "50%" ) 是 50,isFinite 为 true → 返回 50(数字!)。
所以 .width() 对一个 width: 50% 的元素返回 50(数字 50,不是百分比对应的像素)?
不对。回到阶段二:
js
if ( rnumnonpx.test( val ) ) {
if ( !extra ) {
return val;
}
val = "auto";
}
.width() 的 extra 是 "content"(truthy),所以不提前返回 ,而是 val = "auto" → 进入阶段三的 offset 回退 → val = elem.offsetWidth → 返回像素值 ✓
所以 .width() 对百分比宽度元素返回的是 offsetWidth (border-box 像素),再经 boxModelAdjustment 转成 content-box ✓
而 .css( "width" )(extra 为 undefined)→ !extra 为 true → 提前返回 "50%" ✓
这就解释了 10.2 提到的不一致。两种 API 的行为差异根源就在 !extra 这一个判断上。
六个方法的完整行为表(content-box 元素,width:100, padding:10, border:2, margin:5)
| 调用 | extra |
底层 val |
调整 | 结果 |
|---|---|---|---|---|
.width() |
"content" |
100 |
0 |
100 |
.innerWidth() |
"padding" |
100 |
+20 |
120 |
.outerWidth() |
"border" |
100 |
+24 |
124 |
.outerWidth( true ) |
"margin" |
100 |
+24+10 |
134 |
.width( 200 ) |
"content" |
--- | subtract=0 |
写 200px |
.outerWidth( 200 ) |
"border" |
--- | subtract=24 |
写 176px |
.outerWidth( 200, true ) |
"margin" |
--- | subtract=34 |
写 166px |
border-box 元素(width:100 表示 border box 总宽 100):
| 调用 | extra |
底层 val |
调整 | 结果 |
|---|---|---|---|---|
.width() |
"content" |
100 |
-24 |
76 |
.innerWidth() |
"padding" |
100 |
-4 |
96 |
.outerWidth() |
"border" |
100 |
0 |
100 |
.outerWidth( true ) |
"margin" |
100 |
+10 |
110 |
10.7 显隐:isHiddenWithinTree / getDefaultDisplay / showHide
10.7.1 isHiddenWithinTree 与 :hidden 的分野
js
// isHiddenWithinTree reports if an element has a non-"none" display style (inline and/or
// through the CSS cascade), which is useful in deciding whether or not to make it visible.
// It differs from the :hidden selector (jQuery.expr.pseudos.hidden) in two important ways:
// * A hidden ancestor does not force an element to be classified as hidden.
// * Being disconnected from the document does not force an element to be classified as hidden.
// These differences improve the behavior of .toggle() et al. when applied to elements that are
// detached or contained within hidden ancestors (gh-2404, gh-2863).
function isHiddenWithinTree( elem, el ) {
// isHiddenWithinTree might be called from jQuery#filter function;
// in that case, element will be second argument
elem = el || elem;
// Inline style trumps all
return elem.style.display === "none" ||
elem.style.display === "" &&
jQuery.css( elem, "display" ) === "none";
}
(4088--4105)
两个参数的由来 :jQuery#filter 调用伪类/函数时,签名是 fn( elem, index, match )。但 isHiddenWithinTree 也被 showHide 直接调用(isHiddenWithinTree( elem ))。为了兼容两种调用,写了 elem = el || elem。
看 :hidden 的实现(8884 附近)对比:
js
jQuery.expr.pseudos.hidden = function( elem ) {
return !jQuery.expr.pseudos.visible( elem );
};
jQuery.expr.pseudos.visible = function( elem ) {
return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
};
| 判据 | isHiddenWithinTree |
:hidden |
|---|---|---|
只关心 display |
✓ | ✗(visibility: hidden 的元素 offsetWidth 非 0 → :visible) |
| 祖先隐藏会传染 | ✗ | ✓(祖先 display:none → 子孙 offsetWidth 为 0 → :hidden) |
| 游离元素算隐藏 | ✗ | ✓(游离元素 getClientRects() 为空 → :hidden) |
visibility: hidden |
不算隐藏 | 算 :visible(!) |
为什么需要两套判据? 因为用途不同:
:hidden用于选择器------"找出页面上真正看不见的元素"。祖先隐藏、游离、零尺寸都算。isHiddenWithinTree用于显隐操作 ------"我该不该把这个元素的 display 改掉"。如果一个元素的祖先 是display: none,那它自己的display可能还是block,此时"显示它"的正确做法是什么都不做(改它的 display 没用,祖先还藏着)。
注释里引用的 gh-2404、gh-2863 就是这两个场景的 bug:早期 jQuery 的 .show() 会错误地给隐藏祖先下的元素设置 display: block,导致后续祖先显示时元素的内联 display 覆盖了原本的 CSS。
"Inline style trumps all":
js
return elem.style.display === "none" ||
elem.style.display === "" &&
jQuery.css( elem, "display" ) === "none";
- 如果内联
display: none→ 隐藏(无需再看级联) - 如果内联
display为空 → 看计算值(级联/样式表) - 如果内联
display是其他值(如block)→ 不隐藏 (内联覆盖了级联的none)
第三条隐含在逻辑里:第一个条件 === "none" 为 false,第二个条件 === "" 为 false → 返回 false。
这是"内联样式优先级最高"这一 CSS 规则的代码表达。
10.7.2 getDefaultDisplay:问浏览器"这个标签默认怎么显示"
js
var defaultDisplayMap = {};
function getDefaultDisplay( elem ) {
var temp,
doc = elem.ownerDocument,
nodeName = elem.nodeName,
display = defaultDisplayMap[ nodeName ];
if ( display ) {
return display;
}
temp = doc.body.appendChild( doc.createElement( nodeName ) );
display = jQuery.css( temp, "display" );
temp.parentNode.removeChild( temp );
if ( display === "none" ) {
display = "block";
}
defaultDisplayMap[ nodeName ] = display;
return display;
}
(4217--4240)
为什么要问浏览器? 因为"某个标签的默认 display"是浏览器内置样式表 决定的,而不是 JS 能查到的常量。HTML 规范里有建议的默认样式(<div> 是 block、<span> 是 inline、<li> 是 list-item、<table> 是 table、<tr> 是 table-row...),但实际值由 UA 样式表给出,且自定义元素 (<my-widget>)默认是 inline。
jQuery 的做法:创建一个同标签名的临时元素,插到 body 里,读它的 display,再删掉。
为什么必须插到 body? 因为游离元素的 getComputedStyle 在部分浏览器里返回空(9.3 讲过)。插入文档才能获得正确的 UA 样式。
为什么用 elem.ownerDocument 而不是 document? 因为元素可能属于 iframe 的文档。用 ownerDocument 保证在正确的文档里探测。
缓存 defaultDisplayMap[ nodeName ] :nodeName 是大写 的标签名(DIV、SPAN、LI)。缓存避免了同一个标签反复创建/插入/删除。
if ( display === "none" ) display = "block" :某些元素(如 <head>、<style>、<script>、<template>、<base>)的 UA 默认 display 是 none。如果 show() 把这些设成 none 就没意义了,所以兜底为 block。
代价分析 :第一次对某个标签调用 getDefaultDisplay 会:
createElementappendChild(触发样式计算)getComputedStyle(可能触发 reflow)removeChild(触发 reflow)
这是一次昂贵的同步布局。之后同类标签走缓存。所以在循环里第一次 show() 一堆 <div> 是慢的,之后快。
10.7.3 showHide:两遍循环的设计
js
function showHide( elements, show ) {
var display, elem,
values = [],
index = 0,
length = elements.length;
// Determine new display value for elements that need to change
for ( ; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
display = elem.style.display;
if ( show ) {
// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
// check is required in this first loop unless we have a nonempty display value (either
// inline or about-to-be-restored)
if ( display === "none" ) {
values[ index ] = dataPriv.get( elem, "display" ) || null;
if ( !values[ index ] ) {
elem.style.display = "";
}
}
if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
values[ index ] = getDefaultDisplay( elem );
}
} else {
if ( display !== "none" ) {
values[ index ] = "none";
// Remember what we're overwriting
dataPriv.set( elem, "display", display );
}
}
}
// Set the display of the elements in a second loop to avoid constant reflow
for ( index = 0; index < length; index++ ) {
if ( values[ index ] != null ) {
elements[ index ].style.display = values[ index ];
}
}
return elements;
}
(4242--4288)
这是本章架构上最讲究的一个函数 。核心设计是两遍循环。
为什么是两遍
注释说得很清楚:"Set the display of the elements in a second loop to avoid constant reflow"。
如果只用一个循环,每处理一个元素就改一次 style.display,每次都使之前的布局失效。浏览器在下次读布局信息时要把所有失效重算一遍------N 个元素 = N 次布局失效,可能 1 次重算(如果后续不读)或 N 次重算(如果循环里读了)。
两遍循环:第一遍只决定 要设什么值(存在 values[ index ] 里),第二遍只写 。虽然写 N 次仍然会失效 N 次,但第一遍的读取是批量的 ------所有 getComputedStyle 都在元素还处于"原始状态"时完成,不会读到"刚被前一个元素改动影响"的中间态。
更准确地说:第一遍里的 isHiddenWithinTree( elem ) 会调 jQuery.css( elem, "display" )(读计算样式 → 强制 style recalc)。如果读写交替,就是经典的 layout thrashing(布局抖动)。分离读写是标准优化手法。
第一遍的 show 分支
js
display = elem.style.display;
if ( show ) {
if ( display === "none" ) {
values[ index ] = dataPriv.get( elem, "display" ) || null;
if ( !values[ index ] ) {
elem.style.display = "";
}
}
if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
values[ index ] = getDefaultDisplay( elem );
}
}
三种情况:
| 情况 | 内联 display | dataPriv 里有记录? |
处理 |
|---|---|---|---|
| A | "none" |
有(之前 hide 过) | values[i] = 记录的原值;不清空内联 |
| B | "none" |
无 | values[i] = null;清空内联 display (=""),进入下一个 if |
| C | "" |
--- | 检查 isHiddenWithinTree → 若隐藏,values[i] = getDefaultDisplay |
情况 A :.hide() 时存了原 display,.show() 时取出来。注意此时不清空内联 ,因为下一步 values[i] 非 null,第二遍会直接写成原值。
情况 B :内联是 none 但没有记录(用户直接写了 style="display:none")。清空内联,然后看级联:如果级联仍隐藏 → getDefaultDisplay;如果级联给出了 display → values[i] 保持 null,第二遍不写 ✓
values[ index ] = ... || null 的作用:dataPriv.get 返回 undefined 时变成 null,这样 values[index] != null(第二遍的判断)才正确区分"要写"和"不写"。稀疏数组 values 里未赋值的槽是 undefined,undefined != null 是 false(宽松比较)→ 不写 ✓
注意 values 是稀疏数组:只有需要改变的元素才有值。这是空间和语义的双重优化。
第一遍的 hide 分支
js
} else {
if ( display !== "none" ) {
values[ index ] = "none";
// Remember what we're overwriting
dataPriv.set( elem, "display", display );
}
}
简短得多:只要当前不是 none,就记录原值到 dataPriv 并计划设为 none。
注意记录的是 elem.style.display(内联值) ,可能是 ""(表示"没有内联 display")。所以:
js
$( "<div style='display:inline'>" ).hide().show(); // 恢复成 inline
$( "<div>" ).hide().show(); // 恢复成 ""(由级联决定)
dataPriv 是 jQuery 的私有数据存储器 (第 16 章详解)。用它存 display 而不是 data-* 属性,是为了不污染 DOM 属性。
第二遍
js
for ( index = 0; index < length; index++ ) {
if ( values[ index ] != null ) {
elements[ index ].style.display = values[ index ];
}
}
values[ index ] != null 用宽松比较,同时排除 undefined(未赋值)和 null(情况 B 且级联可见)。
注意 :elements[ index ].style 在第一遍已经用 if ( !elem.style ) continue; 检查过了,所以这里不会再遇到没有 style 的元素?
不对------第一遍 continue 时 values[ index ] 保持 undefined,第二遍 undefined != null 为 false → 跳过 ✓ 安全。
show / hide / toggle
js
jQuery.fn.extend( {
show: function() {
return showHide( this, true );
},
hide: function() {
return showHide( this );
},
toggle: function( state ) {
...
}
} );
(4290 起)
.show() / .hide() 都是薄封装。注意 hide 不传第二个参数 ------源码就是 showHide( this ),靠 show 形参取到 undefined(falsy)走 else 分支。写成 showHide( this, false ) 语义等价但不是原文。
toggle 的实现(第 14 章会讲动画版本)在无动画时大致是:
js
toggle: function( state ) {
if ( typeof state === "boolean" ) {
return state ? this.show() : this.hide();
}
return this.each( function() {
if ( isHiddenWithinTree( this ) ) {
jQuery( this ).show();
} else {
jQuery( this ).hide();
}
} );
}
注意 toggle 用的是 isHiddenWithinTree 而不是 :hidden------这就是 10.7.1 讲的分野:toggle 关心的是"元素自己的 display",不是"它在页面上是否可见"。
显隐与动画的关系
.show( 400 ) / .hide( "slow" ) / .toggle( true ) 会走动画路径(第 14 章)。动画结束后会调用 showHide( elements, show ) 做最终的状态设置。所以 showHide 是动画的终点,也是无动画 show/hide 的全部。
dataPriv 的使用与内存
dataPriv.set( elem, "display", value ) 会在元素上挂一个 expando 属性,值是 Data 实例。当元素被移除时,jQuery.cleanData(第 07/16 章)会清理。
一个陷阱 :如果你用原生 elem.remove() 移除元素(不经过 jQuery),dataPriv 的数据不会被清理,造成内存泄漏。这是 jQuery 数据系统的通用问题(第 16 章详述)。
10.8 几何:offset / position / offsetParent
10.8.1 offset():border box 相对文档原点
js
// offset() relates an element's border box to the document origin
offset: function( options ) {
// Preserve chaining for setter
if ( arguments.length ) {
return options === undefined ?
this :
this.each( function( i ) {
jQuery.offset.setOffset( this, options, i );
} );
}
var rect, win,
elem = this[ 0 ];
if ( !elem ) {
return;
}
// Return zeros for disconnected and hidden (display: none) elements (gh-2310)
// Support: IE <=11+
// Running getBoundingClientRect on a
// disconnected node in IE throws an error
if ( !elem.getClientRects().length ) {
return { top: 0, left: 0 };
}
// Get document-relative position by adding viewport scroll to viewport-relative gBCR
rect = elem.getBoundingClientRect();
win = elem.ownerDocument.defaultView;
return {
top: rect.top + win.pageYOffset,
left: rect.left + win.pageXOffset
};
},
(9349--9383)
读路径只有四步:
elem = this[ 0 ]------只读集合的第一个元素 。.offset()不是map,返回单个对象。getClientRects().length为 0 → 返回{ top: 0, left: 0 }(gh-2310,且避免 IE 抛错)getBoundingClientRect()得到视口相对坐标- 加上
win.pageYOffset/win.pageXOffset(滚动量)得到文档相对坐标
为什么用 pageYOffset 而不是 scrollTop?
window.pageYOffset 是标准的滚动偏移量。document.documentElement.scrollTop 在标准模式下有效,在怪异模式(quirks mode)下要用 document.body.scrollTop。pageYOffset 屏蔽了这个差异 ✓
为什么用 elem.ownerDocument.defaultView? 9.2 讲过------IE popup 问题 + iframe 正确性。
写路径:
js
if ( arguments.length ) {
return options === undefined ?
this :
this.each( function( i ) {
jQuery.offset.setOffset( this, options, i );
} );
}
$( el ).offset( undefined ) → arguments.length === 1 → 进入 setter 分支 → options === undefined → 返回 this(no-op,保持链式)。
这是一个精心的设计:如果某个代码路径算出 offset( someMaybeUndefined ),不会因为传了 undefined 而变成 getter。注释说 "Preserve chaining for setter"。
getBoundingClientRect 的语义 :返回的是 border box 相对于视口的位置(含 border,不含 margin)。所以 offset() 注释说 "relates an element's border box to the document origin" ✓
10.8.2 position():margin box 相对 offsetParent 的 padding box
js
// 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() {
if ( !this[ 0 ] ) {
return;
}
var offsetParent, offset, doc,
elem = this[ 0 ],
parentOffset = { top: 0, left: 0 };
// position:fixed elements are offset from the viewport, which itself always has zero offset
if ( jQuery.css( elem, "position" ) === "fixed" ) {
// Assume position:fixed implies availability of getBoundingClientRect
offset = elem.getBoundingClientRect();
} else {
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;
offsetParent = elem.offsetParent || doc.documentElement;
while ( offsetParent &&
offsetParent !== doc.documentElement &&
jQuery.css( offsetParent, "position" ) === "static" ) {
offsetParent = offsetParent.offsetParent || doc.documentElement;
}
if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 &&
jQuery.css( offsetParent, "position" ) !== "static" ) {
// Incorporate borders into its offset, since they are outside its content origin
parentOffset = jQuery( offsetParent ).offset();
parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
}
}
// Subtract parent offsets and element margins
return {
top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
};
},
(9387--9430)
注释里的关键句:"This corresponds to the behavior of CSS absolute positioning"。
CSS 中,position: absolute; top: 10px 的 10px 是元素的 margin box 边缘 到包含块的 padding box 边缘 的距离。jQuery 的 position() 复现的正是这个语义。
分支 A:position: fixed
fixed 元素的参照系是视口 ,而视口的偏移恒为 0。所以直接用 getBoundingClientRect()(视口相对坐标),parentOffset 保持 {0,0} ✓
分支 B:其他 position
offset = this.offset()------ 元素的文档坐标(border box)- 向上找真正的 offsetParent:
js
offsetParent = elem.offsetParent || doc.documentElement;
while ( offsetParent &&
offsetParent !== doc.documentElement &&
jQuery.css( offsetParent, "position" ) === "static" ) {
offsetParent = offsetParent.offsetParent || doc.documentElement;
}
elem.offsetParent 是 DOM 原生属性,返回最近的定位祖先 (position 非 static)或 body/table/td 等。但 jQuery 还要跳过多层:因为 offsetParent 可能是 static 的(比如 body),而 CSS 定位的包含块必须是非 static 的。
循环条件三个:
offsetParent存在- 还不是
documentElement(到顶了) - 它的 position 是
static(不合格,继续往上)
|| doc.documentElement 兜底:offsetParent 为 null 时(fixed 元素、游离元素)用 documentElement。
- 计算 parentOffset:
js
if ( offsetParent && offsetParent !== elem && offsetParent.nodeType === 1 &&
jQuery.css( offsetParent, "position" ) !== "static" ) {
// Incorporate borders into its offset, since they are outside its content origin
parentOffset = jQuery( offsetParent ).offset();
parentOffset.top += jQuery.css( offsetParent, "borderTopWidth", true );
parentOffset.left += jQuery.css( offsetParent, "borderLeftWidth", true );
}
四个守卫条件(都必要):
offsetParent存在offsetParent !== elem(元素自己不是自己的父)nodeType === 1(是元素,不是 document)- position 非 static(找到了合格的包含块)
为什么要加 border 宽度? 注释说 "Incorporate borders into its offset, since they are outside its content origin"。
jQuery( offsetParent ).offset() 返回的是 offsetParent 的 border box 位置(含 border)。而 CSS 定位的参照是 padding box(border box 减去 border)。所以要加上 border 宽度,从 border box 位置移到 padding box 位置 ✓
- 最终计算:
js
return {
top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
};
offset 是元素的 border box 位置。要得到 margin box 位置,需要减去 margin(margin 在 border 外面)✓
10.8.3 offsetParent():三段注释的坦白
js
// This method will return documentElement in the following cases:
// 1) For the element inside the iframe without offsetParent, this method will return
// documentElement of the parent window
// 2) For the hidden or detached element
// 3) For body or html element, i.e. in case of the html node - it will return itself
//
// but those exceptions were never presented as a real life use-cases
// and might be considered as more preferable results.
//
// This logic, however, is not guaranteed and can change at any point in the future
offsetParent: function() {
return this.map( function() {
var offsetParent = this.offsetParent;
while ( offsetParent && jQuery.css( offsetParent, "position" ) === "static" ) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent || documentElement$1;
} );
}
(9432--9452)
这是 jQuery 源码里少见的"坦白式注释":它直接列出了三个已知的"不准确"场景,说这些场景"现实中没出现过,而且当前结果可能更可取",最后加一句"这个逻辑不保证,未来随时可能改变"。
这种注释的价值在于:它告诉维护者这些行为是偶然的,不是契约 。如果你要依赖 offsetParent() 的某个边界行为,注释明确警告了不要这么做。
实现上 :用 this.map(...) 而不是 this.each(...),所以返回一个新的 jQuery 集合 (offsetParent 的集合)。这是 offsetParent() 与 offset()/position() 的重大区别------后者只处理 this[0]。
while 循环跳过多层 static 祖先,与 position() 里的逻辑一致,但没有 !== doc.documentElement 的终止条件。所以:
- 如果
offsetParent是body且body是 static → 继续找body.offsetParent(通常是null)→ 循环结束 →offsetParent || documentElement$1→ 返回documentElement
documentElement$1 而不是 elem.ownerDocument.documentElement :这是一个真实的 bug 源 ------对 iframe 内的元素,会返回主文档的 documentElement(注释里的第 1 条)。jQuery 承认了这一点。
10.8.4 jQuery.offset.setOffset:把"目标位置"翻译成 CSS
js
jQuery.offset = {
setOffset: function( elem, options, i ) {
var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
position = jQuery.css( elem, "position" ),
curElem = jQuery( elem ),
props = {};
// Set position first, in-case top/left are set even on static elem
if ( position === "static" ) {
elem.style.position = "relative";
}
curOffset = curElem.offset();
curCSSTop = jQuery.css( elem, "top" );
curCSSLeft = jQuery.css( elem, "left" );
calculatePosition = ( position === "absolute" || position === "fixed" ) &&
( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
// Need to be able to calculate position if either
// top or left is auto and position is either absolute or fixed
if ( calculatePosition ) {
curPosition = curElem.position();
curTop = curPosition.top;
curLeft = curPosition.left;
} else {
curTop = parseFloat( curCSSTop ) || 0;
curLeft = parseFloat( curCSSLeft ) || 0;
}
if ( typeof options === "function" ) {
// Use jQuery.extend here to allow modification of coordinates argument (gh-1848)
options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
}
if ( options.top != null ) {
props.top = ( options.top - curOffset.top ) + curTop;
}
if ( options.left != null ) {
props.left = ( options.left - curOffset.left ) + curLeft;
}
if ( "using" in options ) {
options.using.call( elem, props );
} else {
curElem.css( props );
}
}
};
(9295--9345)
这是 .offset( { top, left } ) 的实现,也是 jQuery 里最需要几何直觉的一段。
问题 :用户给的是"目标文档坐标",但 CSS 的 top/left 是"相对于包含块的偏移"。两者不是同一个数。
解法 :用增量而不是绝对值。
css
新 top = 目标 top - 当前 offset.top + 当前 CSS top
即:算出"要移动多少",加到现有的 CSS top 上。这样无论包含块是谁,移动量都是对的 ✓
步骤 1:static 元素先变 relative
js
if ( position === "static" ) {
elem.style.position = "relative";
}
top/left 对 static 元素无效 。所以必须先改成 relative(relative 保持元素在正常流中的位置,但允许 top/left 偏移)✓
步骤 2:确定"当前 CSS top/left"
js
calculatePosition = ( position === "absolute" || position === "fixed" ) &&
( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1;
如果元素是 absolute/fixed 且 top 或 left 是 auto,那么CSS 的 top 值不代表实际位置 ------实际位置由"静态位置"(static position,即元素在正常流中应该在的位置)决定。此时要用 curElem.position()(几何计算出的实际偏移)作为基准。
curCSSTop + curCSSLeft 是字符串拼接(如 "auto" + "10px" = "auto10px"),indexOf( "auto" ) > -1 检查任一个是 auto ✓ 这是一个简洁的"两个值里有没有 auto"的判断。
否则(元素非 absolute/fixed,或 top/left 都有具体值),直接用 CSS 值:parseFloat( curCSSTop ) || 0。
步骤 3:函数式 options(gh-1848)
js
if ( typeof options === "function" ) {
options = options.call( elem, i, jQuery.extend( {}, curOffset ) );
}
.offset( function( index, coords ) { return { top: coords.top + 10 }; } )。
jQuery.extend( {}, curOffset ) 是拷贝 ------注释说 "to allow modification of coordinates argument"。如果不拷贝,用户在回调里改 coords 会污染 curOffset,而后面还要用 curOffset.top 算增量 ✓
步骤 4:using 回调
js
if ( "using" in options ) {
options.using.call( elem, props );
} else {
curElem.css( props );
}
.offset( { top: 100, using: function( props ) { $( this ).animate( props ); } } )。
using 让用户接管"如何应用这些 props"------最常见的用法是配合 .animate()。这是 jQuery 的经典扩展点设计(与 jQuery.fn.animate 的 step/complete 同类)。
注意 "using" in options 用的是 in,不是 options.using ------因为 using 可能是 undefined(显式设为 undefined 也算"提供了这个键")。实际上两者在这里差别不大,用 in 更严格。
10.9 滚动:scrollLeft / scrollTop
js
// Create scrollLeft and scrollTop methods
jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
var top = "pageYOffset" === prop;
jQuery.fn[ method ] = function( val ) {
return access( this, function( elem, method, val ) {
// Coalesce documents and windows
var win;
if ( isWindow( elem ) ) {
win = elem;
} else if ( elem.nodeType === 9 ) {
win = elem.defaultView;
}
if ( val === undefined ) {
return win ? win[ prop ] : elem[ method ];
}
if ( win ) {
win.scrollTo(
!top ? val : win.pageXOffset,
top ? val : win.pageYOffset
);
} else {
elem[ method ] = val;
}
}, method, val, arguments.length );
};
} );
(9455--9485)
映射表的作用
js
jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, ... )
| jQuery 方法 | window 上的对应属性 |
|---|---|
scrollLeft |
pageXOffset |
scrollTop |
pageYOffset |
var top = "pageYOffset" === prop; ------ 标记当前处理的是"垂直"还是"水平"。用于后面的 scrollTo 参数选择。
"Coalesce documents and windows"(合并文档与窗口)
js
var win;
if ( isWindow( elem ) ) {
win = elem;
} else if ( elem.nodeType === 9 ) {
win = elem.defaultView;
}
$( window ).scrollTop() 和 $( document ).scrollTop() 等价 ------因为后者被转换为 document.defaultView(即 window)。
这是一个很人性化的设计:用户不需要记住"滚动是 window 的还是 document 的"。
读路径
js
if ( val === undefined ) {
return win ? win[ prop ] : elem[ method ];
}
- window/document →
win.pageYOffset - 普通元素 →
elem.scrollTop
为什么 window 用 pageYOffset 而元素用 scrollTop? 因为 window 没有 scrollTop 属性(Window 接口没有),只有 pageYOffset(以及别名 scrollY)。而元素没有 pageYOffset。两者语义相同(滚动偏移),但属性名不同。
写路径
js
if ( win ) {
win.scrollTo(
!top ? val : win.pageXOffset,
top ? val : win.pageYOffset
);
} else {
elem[ method ] = val;
}
scrollTo( x, y ) 需要两个参数。但我们只想改一个:
- 对
scrollTop(top === true):scrollTo( win.pageXOffset, val )------ 保持水平不变 ✓ - 对
scrollLeft(top === false):scrollTo( val, win.pageYOffset )------ 保持垂直不变 ✓
!top ? val : win.pageXOffset 是第一个参数(x):top 为 false(即 scrollLeft)时取 val,否则取当前 x ✓ top ? val : win.pageYOffset 是第二个参数(y):top 为 true(即 scrollTop)时取 val,否则取当前 y ✓
为什么用 scrollTo 而不是 win.scrollY = val? 因为 pageYOffset / scrollY 是只读 的。设置滚动必须调 scrollTo / scroll / scrollBy。
arguments.length 作为 chainable :$( window ).scrollTop( 0 ) → arguments.length === 1 → truthy → 写模式 ✓;$( window ).scrollTop() → 0 → 读模式 ✓
一个已知的不对称
$( el ).scrollTop() 对元素返回 elem.scrollTop(数字)。但元素必须是可滚动的 (有 overflow: auto/scroll 且内容溢出),否则恒为 0。
而 $( window ).scrollTop() 返回的是文档滚动量 。jQuery 没有为 document.scrollingElement 做特殊处理------因为在标准模式下 document.documentElement.scrollTop 就是文档滚动,而 jQuery 把它 coalesce 成了 window,绕过了这个问题 ✓
与 offset() 的协同
js
// 滚动到某元素
$( "html, body" ).animate( { scrollTop: $( "#target" ).offset().top }, 500 );
这是最常见的用法。offset().top 给出文档坐标,scrollTop 设为该值即可滚到元素顶部。
注意 $( "html, body" ) 而不是 $( "body" )------因为不同浏览器的滚动容器不同(标准模式是 documentElement,怪异模式是 body)。jQuery 的 scrollTop 对 html 和 body 元素都走 elem[ method ] = val 分支,所以两个都设能覆盖所有情况。
10.10 陷阱与边界(18 条)
陷阱 1:.css( "width" ) 与 .width() 返回值不一致
现象:
html
<div id="box" style="width: 50%"></div>
js
$( "#box" ).css( "width" ); // "50%" 字符串
$( "#box" ).width(); // 480 数字(父元素 960 的一半)
根因 :getWidthOrHeight 的阶段二(6077--6082):
js
if ( rnumnonpx.test( val ) ) {
if ( !extra ) {
return val; // .css() 走这里,返回 "50%"
}
val = "auto"; // .width() 走这里,强制 offset 回退
}
.css( "width" ) 的 extra 是 undefined → !extra 为 true → 提前返回字符串。 .width() 的 extra 是 "content" → 走 offset 回退 → 返回 offsetWidth(border box)再转 content box。
后果 :如果你写 $( el ).width() === $( el ).css( "width" ) 判断,永远不相等(类型都不同)。
正确心智模型 :.css( prop ) 问的是"CSS 声明/计算值是什么 ",.width() 问的是"渲染出来多宽"。
陷阱 2:outerWidth( true ) 的 boolean 会关闭写模式
现象:
js
$( el ).outerWidth( true ); // 读,含 margin
$( el ).outerWidth( 100 ); // 写,按 border 盒换算
$( el ).outerWidth( 100, true ); // 写,按 margin 盒换算
根因 :chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" )(9497)。对 outerWidth,defaultExtra 是 ""(falsy),所以看 typeof margin !== "boolean":
margin === true→typeof是"boolean"→ 表达式 false →chainable = false→ 读模式margin === 100→typeof是"number"→ true →chainable = true→ 写模式
后果 :$( el ).outerWidth( false ) 也是读模式(false 也是 boolean),等价于 outerWidth()。而 $( el ).outerWidth( 0 ) 是写模式(设置宽度为 0)。
这是一个靠参数类型区分语义的设计,容易误用。
陷阱 3:box-sizing 会改变 .width() 的含义
现象 :同一个元素,box-sizing 从 content-box 改成 border-box 后,.width() 的返回值变了。
css
width: 100px; padding: 10px; border: 2px;
content-box: .width() = 100, .outerWidth() = 124
border-box: .width() = 76, .outerWidth() = 100
根因 :.width() 的 extra 是 "content",永远返回 content box 宽度。CSS 里 width 属性在 content-box 下指的是 content 宽,在 border-box 下指的是 border box 宽。所以换算方向相反。
后果 :Bootstrap 等框架会设 * { box-sizing: border-box },此时 .width() 返回的数字小于 CSS 里写的 width。很多人以为 .width() 应该等于 CSS 值,这是错的。
规避 :如果你要"CSS 里写的那个值",用 .css( "width" );如果你要"内容区实际多宽",用 .width();如果你要"占多大地方",用 .outerWidth()。
陷阱 4:对隐藏元素读尺寸是昂贵操作
现象 :循环里对 display: none 的元素调用 .width(),性能骤降。
根因 :cssHooks.width.get(6264--6268):
js
return jQuery.css( elem, "display" ) === "none" ?
swap( elem, cssShow, function() {
return getWidthOrHeight( elem, dimension, extra );
} ) :
getWidthOrHeight( elem, dimension, extra );
每次都要:读 display → 写三个内联样式 → 读 offsetWidth(强制 reflow)→ 恢复三个内联样式(又一次 reflow)。
量化 :一次 swap + getWidthOrHeight 约 0.1--1 ms。100 个隐藏元素 = 10--100 ms。
规避:批量处理时先显示容器(或移到视口外),读完全部尺寸后再隐藏:
js
// 慢
$( ".hidden-item" ).each( function() { sizes.push( $( this ).width() ); } );
// 快
var $p = $( ".hidden-item" ).parent();
$p.show();
$( ".hidden-item" ).each( function() { sizes.push( $( this ).width() ); } );
$p.hide();
陷阱 5:inline 元素的 .width() 走 offset 回退
现象:
html
<span id="s">hello</span>
js
$( "#s" ).css( "width" ); // "auto"
$( "#s" ).width(); // 37(offsetWidth)
根因 :getComputedStyle 对未显式设宽的 inline 元素返回 "auto"(gh-3571)。getWidthOrHeight 的 val === "auto" 条件成立 → 走 offset 回退 → val = elem.offsetWidth。
注意 :offsetWidth 是 border box ,所以回退后 valueIsBorderBox = true,后续 boxModelAdjustment 会按 border box 解释。对 .width()(extra = "content")会减去 padding + border。inline 元素的 padding 在垂直方向不参与布局,但水平方向参与,所以结果仍有意义。
陷阱 6:.width( "50%" ) 会触发一次"先写后读"
现象 :.width( "50%" ) 比 .width( 500 ) 慢。
根因 :cssHooks.width.set(6290--6295):
js
if ( subtract && ( matches = rcssNum.exec( value ) ) &&
( matches[ 3 ] || "px" ) !== "px" ) {
elem.style[ dimension ] = value;
value = jQuery.css( elem, dimension );
}
当 subtract 非零(需要盒模型换算)且值不是 px 时,jQuery 必须先把 50% 写进去让浏览器算出实际 px,才能做减法。这是一次额外的写入 + 读取。
注意条件 :subtract 必须非零。对 .width()(extra = "content")在 content-box 元素上,boxModelAdjustment 早退返回 0 → subtract = 0 → 跳过换算 。所以 .width( "50%" ) 是快的,.outerWidth( "50%" ) 是慢的。
陷阱 7:SVG 元素没有 offsetWidth
现象 :$( svgRect ).width() 返回的值与预期不符。
根因 :valueIsBorderBox = offsetProp in elem(6110)。SVG 元素(SVGElement)不继承 HTMLElement 的 offsetWidth/offsetHeight,所以 offsetProp in elem 为 false,回退不生效,val 保持 curCSS 的计算值,且被当作 content box 解释。
源码注释(6108--6109)明说:"Where not available (e.g., SVG), assume unreliable box-sizing and interpret the retrieved value as a content box dimension."
后果 :对 SVG 元素,.width() 基本等价于 parseFloat( getComputedStyle( el ).width )。
陷阱 8:.offset() 只处理集合的第一个元素
现象:
js
$( "div" ).offset(); // 只返回第一个 div 的位置,不是数组
根因 :var elem = this[ 0 ];(9362),且读分支直接 return { top, left }。
对比 :.offsetParent() 用 this.map(...),返回集合;.position() 也只用 this[0]。
规避 :要批量取位置用 .map():
js
var positions = $( "div" ).map( function() { return $( this ).offset(); } ).get();
陷阱 9:.offset() 对隐藏/游离元素返回 { top: 0, left: 0 }
现象:
js
$( "#hidden" ).offset(); // { top: 0, left: 0 }
根因 :9372--9374 的 if ( !elem.getClientRects().length ) { return { top: 0, left: 0 }; }(gh-2310)。注释还提到:在 IE 上对游离节点调 getBoundingClientRect 会抛错,所以这个守卫同时是防崩溃。
后果 :{ top: 0, left: 0 } 与"元素真的在左上角"无法区分。需要区分时必须先判断可见性。
陷阱 10:.offset( undefined ) 返回 this 而不是读
现象:
js
var opts = maybeUndefined;
$( el ).offset( opts ); // 若 opts 是 undefined,返回 jQuery 对象,no-op
根因 :9353--9359 的 if ( arguments.length ) { return options === undefined ? this : ... }。只要传了参数(哪怕 undefined),就进 setter 分支;options === undefined 时直接返回 this 保持链式。
设计意图(注释 "Preserve chaining for setter"):避免"根据运行时值决定读/写"的代码意外变成读操作,破坏链式调用。
陷阱 11:position() 对 fixed 元素用 getBoundingClientRect
现象 :position: fixed 元素的 .position() 返回的是视口相对 坐标,而普通元素是包含块相对坐标。
根因:9397--9400:
js
if ( jQuery.css( elem, "position" ) === "fixed" ) {
offset = elem.getBoundingClientRect();
}
parentOffset 保持 { top: 0, left: 0 }(视口偏移恒为 0)。
语义一致性 :这是正确的------fixed 元素的包含块就是视口,所以"相对包含块的坐标"就是"相对视口的坐标" ✓
后果 :页面滚动时,fixed 元素的 .position() 不变,而普通元素的 .position() 也不变(因为都是相对包含块)。但 fixed 元素的 .offset() 会随滚动变化(因为 offset 是文档坐标)。
陷阱 12:offsetParent() 对 iframe 内元素返回主文档的 documentElement
现象 :iframe 内元素(且无合格 offsetParent)的 .offsetParent() 返回主页面 的 <html>。
根因 :9450 用 documentElement$1(模块级常量,指向主文档的 document.documentElement)而不是 elem.ownerDocument.documentElement。
jQuery 的态度:源码注释(9432--9441)坦白列出了三个已知不准确的场景,并说"这些异常从未在现实中出现过,当前结果可能更可取;但此逻辑不保证,未来可能改变"。
规避 :需要精确结果时用原生 elem.offsetParent。
陷阱 13:scrollTop 对不可滚动元素恒为 0
现象:
js
$( "#nonScrollable" ).scrollTop( 100 );
$( "#nonScrollable" ).scrollTop(); // 0
根因 :设置 elem.scrollTop = 100 时,如果元素不可滚动(overflow: visible 或内容未溢出),浏览器会把值钳到 0。这是 DOM 行为,不是 jQuery 的问题。
规避:先把元素设为可滚动:
js
$( "#box" ).css( "overflow", "auto" );
$( "#box" ).scrollTop( 100 );
陷阱 14:$( document ).height() 可能出现的偏差
现象 :$( document ).height() 返回值可能大于真实内容高度。
根因 :9517--9521 取 5 个值的 Math.max:
js
Math.max(
elem.body[ "scroll" + name ], doc[ "scroll" + name ],
elem.body[ "offset" + name ], doc[ "offset" + name ],
doc[ "client" + name ]
)
如果 body 有 margin(浏览器默认 8px),body.offsetHeight 会比内容高度小,但 body.scrollHeight 可能受 margin 折叠影响而偏大。取 max 是保守策略------宁可偏大也不偏小(因为常见用途是"判断内容是否超出一屏")。
规避 :要精确值用 document.documentElement.scrollHeight。
陷阱 15:$( window ).width() 不含滚动条,outerWidth() 含
现象:页面有垂直滚动条时:
js
$( window ).width(); // 1920 - 15 = 1905(视口宽,不含滚动条)
$( window ).outerWidth(); // 1920(window.innerWidth,含滚动条)
$( window ).innerWidth(); // 1905(与 .width() 相同!)
根因:9506--9508:
js
return funcName.indexOf( "outer" ) === 0 ?
elem[ "inner" + name ] :
elem.document.documentElement[ "client" + name ];
只有以 "outer" 开头的方法走 window.innerWidth。gh-1729 是这个行为的 bug 编号。
后果 :$( window ).innerWidth() 不等于 window.innerWidth。这是一个命名冲突导致的反直觉行为。
陷阱 16:.hide() 之后再 .show() 不一定恢复原状
现象:
html
<div id="a" style="display: inline">x</div>
<div id="b">x</div> <!-- CSS: #b { display: inline-block } -->
js
$( "#a" ).hide().show(); // 恢复成 inline ✓(dataPriv 记的是 "inline")
$( "#b" ).hide().show(); // dataPriv 记的是 "",show 时清空内联 → 回落到 CSS 的 inline-block ✓
$( "#b" ).css( "display", "block" ).hide().show(); // 记的是 "block" ✓
根因 :showHide 的 hide 分支记 dataPriv.set( elem, "display", display ),其中 display 是 elem.style.display(内联值)。show 时取出该值写回。如果原内联值是 "",写回 "" 等于删除内联声明,回落到级联 ✓
真正的陷阱 :如果 .hide() 之后修改了 CSS 规则 ,.show() 会用旧值覆盖新规则。以及:
js
$( el ).hide();
el.style.display = "flex"; // 绕过 jQuery 直接改
$( el ).show(); // 会写成 dataPriv 里的旧值,覆盖 flex!
陷阱 17:getDefaultDisplay 首次调用昂贵,且对 <script> 返回 block
现象 :第一次 .show() 一个 <div> 会创建 + 插入 + 读取 + 删除一个临时元素。
根因 :getDefaultDisplay(4219--4240)没有缓存时的完整流程。defaultDisplayMap[ nodeName ] 缓存后不再重复。
另一个后果 :<script>、<style>、<template>、<head> 的 UA 默认 display 是 none,getDefaultDisplay 会兜底返回 "block"(4234--4236)。所以 $( "script" ).show() 会让它变成 display: block------虽然 script 仍然不可见(因为它没有渲染内容),但会占据布局空间。
陷阱 18:负 margin 与 outerWidth( true )
现象:
html
<div style="width:100px; margin:-20px"></div>
js
$( el ).outerWidth( true ); // 100 + (-20*2) = 60,而不是 100
根因 :boxModelAdjustment 的 marginDelta 分支(6006--6008):
js
if ( box === "margin" ) {
marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles );
}
jQuery.css( ..., true ) 返回带符号的数字,所以 margin: -20px 得到 -20,marginDelta = -40。
delta + marginDelta = 0 + (-40) = -40,100 - 40 = 60 ✓
这就是 gh-3982 的意义 :如果 margin 混在 delta 里,会被滚动条公式的 Math.max( 0, ... ) 钳掉。分开累计后负值得以保留 ✓
注意 :getWidthOrHeight 的最后是 ( val + 调整量 ) + "px",如果调整量绝对值大于 val,会得到负数(如 -40px)。只有 setPositiveNumber(写通道)才做非负钳制,读通道不钳制。
10.11 全章总结
核心组件表
| 组件 | 行号 | 职责 | 关键设计 |
|---|---|---|---|
jQuery.cssHooks |
6138 | 样式钩子的公开扩展点 | 空对象起步,鸭子类型协议 |
cssHooks.width / cssHooks.height |
6257--6300 | 尺寸读写钩子 | get 在隐藏时用 swap;set 做单位统一化 |
cssHooks.margin/padding/borderWidth |
6303--6328 | 简写展开 + 非负钳制 | expand 用 `parts[i] |
getWidthOrHeight |
6061--6132 | 尺寸主入口 | 六条路径、四个 offset 回退条件 |
boxModelAdjustment |
5990--6059 | 盒模型加减 + 滚动条 | i 起始值 + 步长 2 表达方向对;margin 单独累计 |
setPositiveNumber |
5978--5988 | 写通道值钳制 | Math.max( 0, ... ),第一个参数未使用 |
jQuery.fn.css |
6330--6353 | 公开 API | access 四态 + 数组读复用 styles |
| 尺寸方法族(6 个) | 9487--9534 | width/innerWidth/outerWidth 及高变体 |
二重 jQuery.each 代码生成 |
isHiddenWithinTree |
4088--4105 | display 判据(不传染) | 与 :hidden 分野 |
getDefaultDisplay |
4217--4240 | 标签默认 display 探测 | 建同标签临时元素插 body 读值;defaultDisplayMap 缓存 |
showHide |
4242--4288 | 显隐 | 两遍循环避免布局抖动;dataPriv 存原值 |
jQuery.fn.offset |
9349--9383 | border box → 文档原点 | gBCR + pageX/YOffset |
jQuery.fn.position |
9387--9430 | margin box → 包含块 padding box | fixed 特判;向上跳过 static |
jQuery.fn.offsetParent |
9442--9452 | 定位父 | 坦白式注释列出 3 个已知不准场景 |
jQuery.offset.setOffset |
9295--9345 | 位置写入 | 增量翻译;using 回调 |
scrollLeft / scrollTop |
9455--9485 | 滚动 | window/document 归一;scrollTo 双参数 |
工程经验清单
-
用数据驱动生成 API 。六个尺寸方法由二重
jQuery.each生成,键是defaultExtra、值是方法名。改动只需改那张映射表。这是 jQuery 的标准代码生成手法,在第 07、08 章也反复出现。 -
方向对用"起始索引 + 步长"表达 。
var i = dimension === "width" ? 1 : 0; for ( ; i < 4; i += 2 )------ 用cssExpand的索引奇偶性一次性覆盖两个方向。代价是可读性,收益是零分支。 -
中间量与结果量要分开 。
boxModelAdjustment里的extra(border 宽度,参与滚动条公式但不进结果)与marginDelta(在滚动条计算之后才加)都是为了让同一个公式能服务于不同目标盒。 -
读通道不钳制,写通道钳制 。
getWidthOrHeight可以返回"-40px",setPositiveNumber保证非负。语义不同:读是"报告事实",写是"构造合法 CSS"。 -
两遍循环分离读写 。这是避免 layout thrashing 的标准手法,在
showHide(4249 决定 / 4281 写入)里体现得最清楚。任何"读一批 → 写一批"的 DOM 批处理都应该照此组织。 -
哨兵变量兼作状态标记 。
values[ index ] = ... || null把undefined(未赋值)和null(不需改变)区分开,稀疏数组同时表达了"要不要写"和"写什么"。 -
不确定的行为要写下来 。
offsetParent()的三段注释是范例:列出已知不准的场景、说明为什么当前结果可接受、警告未来可能改变。这比沉默的"正确"更有价值。 -
无法解决的缺陷要承认 。
support.reliableColDimensions对 IE 直接返回 true 并注释 "we accept this limitation"。诚实标注边界,比写一堆无效 workaround 更负责。 -
扩展点要留给用户接管 。
setOffset的using回调、cssHooks的get/set------jQuery 在"我算好参数,你怎么用随你"的位置留接口,这是它生态繁荣的原因之一。 -
同一个概念在不同模块可能有不同判据 。
:hidden(几何)vsisHiddenWithinTree(display)不是重复,而是用途驱动的分化。理解"为什么有两套"比记住"有两套"重要。
"怪代码"解释表
| 代码 | 位置 | 为什么这么写 |
|---|---|---|
| `parts[ i ] | parts[ i - 2 ] | |
if ( prefix !== "margin" ) { ...set = setPositiveNumber } |
6325--6327 | margin 可为负,不能钳制 |
| `boxSizingNeeded = isIE | extra` | |
"Fake content-box until we know it's needed" |
6067 注释 | 用便宜的假值让后续走"不调整"分支 |
val = "auto" |
6081 | 强制进入 offset 回退(非 px 值无法做盒模型加减) |
valueIsBorderBox = offsetProp in elem |
6110 | SVG 没有 offsetWidth;有的话它恒是 border box |
i = dimension === "width" ? 1 : 0 + i += 2 |
5991 / 6001 | 用索引奇偶性选方向对 |
Math.ceil( x - 0.5 ) |
6046--6051 | 等价于 Math.round,配合 Math.max( 0, ... ) 防负 |
| `) ) | 0;` | |
extra += border(不进 delta) |
6022 | 参与滚动条公式但不参与结果 |
marginDelta 分开累计 |
6006 | gh-3982:负 margin 不能被 Math.max(0,...) 钳掉 |
_elem 参数未使用 |
5978 | 钩子签名兼容(elem, value, extra) |
"outer" + name 且 defaultExtra: "" |
9492 | 空串键是 falsy,让 `defaultExtra |
funcName.indexOf( "outer" ) === 0 |
9506 | 区分 outerWidth(含滚动条)与其他 |
Math.max( 5 个值 ) |
9517 | 文档高度没有标准答案,取保守上界 |
chainable ? margin : undefined |
9531 | 防止 outerWidth( true ) 的 true 被当 CSS 值写入 |
| `elem = el | elem` | |
elem.style.display === "" |
4103 | 内联为空才看级联(内联优先) |
| `values[ index ] = ... | null` | |
doc.body.appendChild( doc.createElement( nodeName ) ) |
4229 | 必须插入文档才能获得 UA 默认样式 |
offset.top - parentOffset.top - marginTop |
9427 | border box → margin box 要减 margin |
parentOffset.top += borderTopWidth |
9420 | border box → padding box 要加 border |
( curCSSTop + curCSSLeft ).indexOf( "auto" ) > -1 |
9311 | 字符串拼接查两个值里有没有 auto |
!top ? val : win.pageXOffset |
9476 | scrollTo 需要双参数,只改一个时另一个保持 |
| `offsetParent | documentElement$1` |
附录 A:本章源码索引
| 行号 | 内容 |
|---|---|
| 4088--4105 | isHiddenWithinTree |
| 4217--4240 | defaultDisplayMap / getDefaultDisplay |
| 4242--4288 | showHide |
| 4290+ | jQuery.fn.extend( { show, hide, toggle } ) |
| 5978--5988 | setPositiveNumber |
| 5990--6059 | boxModelAdjustment |
| 6061--6132 | getWidthOrHeight |
| 6134--6138 | jQuery.cssHooks = {} |
| 6257--6300 | cssHooks.height / cssHooks.width |
| 6303--6328 | cssHooks.margin / padding / borderWidth |
| 6330--6353 | jQuery.fn.css |
| 8881 / 8884 | :visible / :hidden 伪类 |
| 9289--9293 | jQuery.expr.pseudos.animated |
| 9295--9345 | jQuery.offset.setOffset |
| 9347--9453 | offset / position / offsetParent |
| 9455--9485 | scrollLeft / scrollTop |
| 9487--9534 | 六个尺寸方法 |
附录 B:速查表
六个尺寸方法对照(content-box 元素:width 100 / padding 10 / border 2 / margin 5)
| 方法 | extra |
读结果 | 写 200 时实际写入 |
|---|---|---|---|
.width() |
"content" |
100 |
200px |
.innerWidth() |
"padding" |
120 |
180px |
.outerWidth() |
"border" |
124 |
176px |
.outerWidth( true ) |
"margin" |
134 |
166px |
border-box 元素(width 100 表示 border box 总宽)
| 方法 | 读结果 |
|---|---|
.width() |
76 |
.innerWidth() |
96 |
.outerWidth() |
100 |
.outerWidth( true ) |
110 |
boxModelAdjustment 的四盒换算
| 起点 \ 目标 | content | padding | border | margin |
|---|---|---|---|---|
| content-box | 0 | +2P | +2P+2B | +2P+2B+2M |
| border-box | -2P-2B | -2B | 0 | +2M |
(P/B/M 为单侧值;content-box 目标为 padding/border/margin 时还要加上滚动条 gutter)
三个几何 API 的参照系
| API | 参照系 | 测量对象 |
|---|---|---|
.offset() |
文档原点 | 元素 border box |
.position() |
包含块 padding box | 元素 margin box |
.offsetParent() |
--- | 定位父元素(集合) |
.scrollTop() |
--- | 滚动偏移 |
附录 C:思考题
-
.css( "width" )返回"50%",.width()返回480。 这两个值在源码里由哪一行分支决定?如果要让.css( "width" )也返回像素值,需要改哪里?会带来什么副作用? -
boxModelAdjustment里extra变量(记录 border 宽度但不进delta)只在box === "padding"分支被赋值。 为什么其他分支不需要它?请从滚动条公式推导。 -
为什么
marginDelta必须在return delta + marginDelta时才加,而不能直接累加到delta? 构造一个具体的反例(含负 margin 和滚动条)说明。 -
getWidthOrHeight的四个 offset 回退条件里,isIE && isBorderBox这一条。 IE 的什么行为导致必须回退?如果去掉这一条,在 IE 上会出现什么现象? -
valueIsBorderBox = offsetProp in elem。 除了 SVG,还有哪些元素没有offsetWidth?jQuery 对这些元素的尺寸处理是否符合 CSS 语义? -
showHide的两遍循环真的减少 reflow 吗? 第一遍里有isHiddenWithinTree(读getComputedStyle)和elem.style.display = ""(写),这本身不就是读写交替吗?请分析 jQuery 的做法是否真的达到了目的。 -
getDefaultDisplay把<script>的默认 display 兜底成block。 这是好设计吗?如果改成返回"none"会怎样? -
offsetParent()用documentElement$1(主文档)而非elem.ownerDocument.documentElement。 请给出一个复现步骤,证明这个 bug 在 iframe 场景下确实存在。为什么 jQuery 选择不修? -
setOffset用增量而不是绝对值计算新的top/left。 如果改用绝对值(直接props.top = options.top),在什么情况下会出错? -
scrollLeft/scrollTop用win.scrollTo( x, y )而不是win.scrollX = val。 除了"属性只读"这个理由,还有别的考虑吗?(提示:scrollTo支持behavior: "smooth") -
chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" )。 这个表达式让outerHeight( false )变成读操作。这是有意的还是意外?请分析false与0的行为差异并评价。 -
假设你要给 jQuery 加一个
.contentBoxWidth()方法 (永远返回 content box 宽度,无论box-sizing如何)。请写出实现,并说明它应该插入jQuery.each的哪个位置。
下一章预告
第 09、10 章完成了"元素长什么样、在哪里、有多大"这一层。接下来进入 jQuery 最庞大、最精妙的模块------事件系统。
第 11 章 事件系统(上):注册与分发 将覆盖:
jQuery.event.add:on的参数洗牌(on( types, selector, data, fn )的四种重载如何归一化)handleObj结构 :一次on调用在元素上留下的数据结构,以及guid的作用delegateCount:为什么委托处理器必须排在数组前面,以及这个设计如何用一次排序换取分发时的零判断dispatch:事件分发的主循环,从native event到jQuery.Event到处理器链handlers:委托路径的冒泡遍历,jQuery.find.matchesSelector在每层祖先上的调用- 命名空间 :
rtypenamespace正则与.off( "click.myNS" )的匹配语义 off的重载:五种调用形态如何映射到同一套删除逻辑special协议 :setup/teardown/add/remove四个钩子如何在注册期介入