jQuery源码解析(4)—— css样式、定位属性
2016-04-04 23:14
711 查看
闲话
原计划是没有这篇博文的,研究animation源码的时候遇到了css样式这个拦路虎。比如jQuery支持“+=10”、“+=10px”定义一个属性的增量,但是有的属性设置时可以支持数字,有的必须有单位;在对属性当前值读取时,不同的浏览器可能返回不同的单位值,无法简单的相加处理;在能否读取高宽等位置信息上,还会受到display状态的影响;不同浏览器,相同功能对应的属性名不同,可能带有私有前缀等等。众多的疑问,让我决定先破拆掉jQuery的样式机制。
(本文采用 1.12.0 版本进行讲解,用 #number 来标注行号)
css
jQuery实现了一套简单统一的样式读取与设置的机制。$(selector).css(prop)读取,$(selector).css(prop, value)写入,也支持对象参数、映射写入方式。厉害的是,这种简单高效的用法,完全不用考虑兼容性的问题,甚至包括那些需要加上前缀的css3属性。/* 读取 */ $('#div1').css('lineHeight') /* 写入 */ $('#div1').css('lineHeight', '30px') // 映射(这种写法其实容易产生bug,不如下面一种,后文会讲到) $('#div1').css('lineHeight', function(index, value) { return (+value || 0) + '30px'; }) // 增量(只支持+、-,能够自动进行单位换算,正确累加) $('#div1').css('lineHeight', '+=30px') // 对象写法 $('#div1').css({ 'lineHeight': '+=30px' , 'fontSize': '24px' })
如何统一一个具有众多兼容问题的系统呢?jQuery的思路是抽象一个标准化的流程,然后对每一个可能存在例外的地方安放钩子,对于需要例外的情形,只需外部定义对应的钩子即可调整执行过程,即标准化流程 + 钩子。
下面我们来逐个击破!
1、access
jQuery.fn.css( name, value )的功能是对样式的读取和写入,属于外部使用的外观方法。内部的核心方法是
jQuery.css( elem, name, extra, styles )、
jQuery.style( elem, name, value, extra )。
jq中链式调用、对象写法、映射、无value则查询这些特点套用在了很多API上,分成两类。比如第一类:jQuery.fn.css(name, value)、第二类:jQuery.fn.html(value),第二类不支持对象参数写法。jq抽离了不变的逻辑,抽象成了
access( elems, fn, key, value, chainable, emptyGet, raw )入口。
难点(怪异的第二类)
第一类(有key,bulk = false)
普通(raw):对elems(一个jq对象)每一项->fn(elems[i], key, value)
映射(!raw):对elems每一项elems[i],求得key属性值val=fn(elems[i], key),执行map(即value)函数value.call( elems[ i ], i, val )得到返回值re,执行fn(elems[i], key, re)
取值(!value):仅取第一项fn( elems[ 0 ], key )
第二类(无key,bulk = true)
普通(raw):直接fn.call(elems, value)
映射(!raw):对elems每一项elems[i],求得值val=fn.call( jQuery( elems[i] )),执行map(即value)函数value.call( jQuery(elems[ i ]), val )得到返回值re,执行fn.call( jQuery( elems[i] ), re)
取值(!value):取fn.call( elems )
正是这两类的不同造成了access内部逻辑的难懂,下面代码中第二类进行fn封装,就是为了bulk->!raw->map能够与第一类使用同样的逻辑。两类的使用方法,包括映射参数写法都是不同的。有value均为链式调用chainable=true
// #4376 var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { var i = 0, length = elems.length, // 确定哪一类,false为第一类 bulk = key == null; // 第一类对象写法,为设置,开启链式 if ( jQuery.type( key ) === "object" ) { chainable = true; // 拆分成key-value式写法,静待链式返回 for ( i in key ) { access( elems, fn, i, key[ i ], true, emptyGet, raw ); } // value有值,为设置,开启链式 } else if ( value !== undefined ) { chainable = true; if ( !jQuery.isFunction( value ) ) { raw = true; } if ( bulk ) { // bulk->raw 第二类普通赋值,静待链式返回 if ( raw ) { fn.call( elems, value ); fn = null; // bulk->!raw 第二类map赋值,封装,以便能使用第一类的式子 } else { bulk = fn; fn = function( elem, key, value ) { return bulk.call( jQuery( elem ), value ); }; } } if ( fn ) { for ( ; i < length; i++ ) { // 第一类raw普通,!raw映射。封装后的第二类共用映射方法 fn( elems[ i ], key, raw ? value : value.call( elems[ i ], i, fn( elems[ i ], key ) ) ); } } } return chainable ? // 赋值,链式 elems : // 取值 bulk ? // 第二类 fn.call( elems ) : // 第一类 length ? fn( elems[ 0 ], key ) : emptyGet; };
读取依赖window上的getComputedStyle方法,IE6-8依赖元素的currentStyle方法。样式的写入依赖elem.style。
2、jQuery.fn.css
jQuery.fn.css( name, value )为什么会有两个核心方法呢?因为样式的读取和写入不是同一个方式,而写入的方式有时候也会用来读取。读:依赖window上的getComputedStyle方法,IE6-8依赖元素的currentStyle方法。内联外嵌的样式都可查到
写:依赖elem.style的方式。而elem.style方式也可以用来查询的,但是只能查到内联的样式
因此封装了两个方法
jQuery.css( elem, name, extra, styles )、
jQuery.style( elem, name, value, extra ),前者只读,后者可读可写,但是后者的读比较鸡肋,返回值可能出现各种单位,而且还无法查到外嵌样式,因此jQuery.fn.css方法中使用前者的读,后者的写
// #7339 jQuery.fn.css = function( name, value ) { // access第一类用法,fn为核心函数的封装,直接看返回值 return access( this, function( elem, name, value ) { var styles, len, map = {}, i = 0; // 增加一种个性化的 取值 方式。属性数组,返回key-value对象 // 第一类取值,只取elems[0]对应fn执行的返回值 if ( jQuery.isArray( name ) ) { styles = getStyles( elem ); len = name.length; for ( ; i < len; i++ ) { // false参数则只返回未经处理的样式值,给定了styles则从styles对象取样式 // 下面会单独讲jQuery.css 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 ); };
3、属性名兼容
第一步:jq支持驼峰和’-‘串联两种写法,会在核心方法中统一转化为小驼峰形式第二步:不同浏览器的不同属性名兼容,如float为保留字,标准属性是cssFloat,IE中使用styleFloat(当前版本IE已抛弃)。查看是否在例外目录中
第三步:css3属性支持程度不一,有的需要加上私有前缀才可使用。若加上私有前缀才能用,添加到例外目录中方便下次拿取
// #83,#356,第一步,小驼峰 rmsPrefix = /^-ms-/, rdashAlpha = /-([\da-z])/gi, fcamelCase = function( all, letter ) { return letter.toUpperCase(); }; // #356 camelCase = function( string ) { return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); }, // #7083,第二步 cssProps: { // normalize float css property "float": support.cssFloat ? "cssFloat" : "styleFloat" } // #6854,第三步 cssPrefixes = [ "Webkit", "O", "Moz", "ms" ], emptyStyle = document.createElement( "div" ).style; // return a css property mapped to a potentially vendor prefixed property function vendorPropName( name ) { // 查询无前缀驼峰名是否支持 if ( name in emptyStyle ) { return name; } // 首字母变为大写 var capName = name.charAt( 0 ).toUpperCase() + name.slice( 1 ), i = cssPrefixes.length; // 查找是否有支持的私有前缀属性 while ( i-- ) { name = cssPrefixes[ i ] + capName; if ( name in emptyStyle ) { return name; } } }
4、jQuery.css、jQuery.style
jq的样式机制之所有复杂,因为在核心方法功能的设计上,考虑了非必要的“易用”、“兼容”、“扩展”。使得设置更灵活、输出更一致、支持累加换算、支持拓展的功能。由于对下面特性的支持,因此对样式的取值抽象出了核心逻辑curCSS( elem, name, computed ),一来是逻辑划分更清晰,内部更可根据需要酌情选择使用这两者
易用
1、为了提高易用性,jQuery.style()可以自动为设置值加上默认单位’px’,由于有些属性值可以为数字,因此定义了
cssNumber的列表,列表中的项目不会加上默认单位。
2、允许增量’+=20px’式写法,由于采用jQuery.css获取的初始值单位有可能不同,因此封装了一个自动单位换算并输出增量后最终结果的函数
adjustCSS()
兼容
并不是每个属性都能返回预期的值。
1、比如opacity在IE低版本是filter,用jQuery.style方式取值时需要匹配其中数字,结果跟opacity有100倍差距,而且设置的时候alpha(opacity=num)的形式也太独特。
2、比如定位信息会因为元素display为none等状态无法正确获取到getBoundingClientRect()、offsetLeft、offsetWidth等位置信息及大小,而且对于自适应宽度无法取得宽高信息。
3、比如一些浏览器兼容问题,导致某些元素返回百分比等非预期值。
jQuery.cssHooks是样式机制的钩子系统。可以对需要hack的属性,添加钩子,在jQuery.css、jQuery.style读取写入之前,都会先看是否存在钩子并调用,然后决定是否继续下一步还是直接返回。通过在
set、
get属性中定义函数,使得行为正确一致。
扩展
1、返回值:jQuery.css对样式值的读取,可以指定对于带单位字符串和”auto”等字符串如何返回,新增了
extra参数。为”“(不强制)和true(强制)返回去单位值,false不做特殊处理直接返回。
2、功能扩展:jq允许直接通过innerWidth()/innerHeight()、outerWidth()/outerHeight()读取,也支持赋值,直接调整到正确的宽高。这是通过extra指定padding、border、margin等字符串做到的
3、cssHooks.expand:对于margin、padding、borderWidth等符合属性,通过扩展expand接口,可以得到含有4个分属性值的对象。
bug
使用字符串’30’和数字30的效果有区别。对于不能设为数字的,数字30自动加上px,字符串的却不会。
下面adjustCSS换算函数中也提到一个bug,下面有描述。
建议:adjustCSS函数本身就可以处理增量和直接量两种情况,type===’string’判断的地方不要ret[ 1 ],以解决第一个问题。adjustCSS返回一个数组,第一个为值,第二个为单位,这样就防止第二个bug。
// #4297,pnum匹配数字,rcssNum -> [匹配项,加/减,数字,单位] var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; var rcssNum = new RegExp( "^(?:([+-])=)(" + pnum + ")([a-z%]*)$", "i" ); // #6851 cssNormalTransform = { letterSpacing: "0", fontWeight: "400" } // #7090,核心方法 jQuery.extend( { // 支持数字参数的属性列表,不会智能添加单位 cssNumber: { "animationIterationCount": true, "columnCount": true, "fillOpacity": true, "flexGrow": true, "flexShrink": true, "fontWeight": true, "lineHeight": true, "opacity": true, "order": true, "orphans": true, "widows": true, "zIndex": true, "zoom": true }, // elem.style方式读写 style: function( elem, name, value, extra ) { // elem为文本和注释节点直接返回 if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { return; } /** * ---- 1、name修正,属性兼容 ---- */ var ret, type, hooks, // 小驼峰 origName = jQuery.camelCase( name ), style = elem.style; // 例外目录、私有前缀 name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName ); // 钩子 // 先name、后origName使钩子更灵活,既可统一,又可单独 hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; /** * ---- 2、elem.style方式 - 赋值 ---- */ if ( value !== undefined ) { type = typeof value; // '+='、'-='增量运算 // Convert "+=" or "-=" to relative numbers (#7345) if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { // adjustCSS对初始值和value进行单位换算,相加/减得到最终值(数值) value = adjustCSS( elem, name, ret ); // 数值需要在下面加上合适的单位 type = "number"; } // Make sure that null and NaN values aren't set. See: #7116 if ( value == null || value !== value ) { return; } // 数值和'+=xx'转换的数值,都需要加上单位。cssNumber记录了可以是数字的属性,否则默认px // ret[3]为'+=xx'原本匹配的单位 if ( type === "number" ) { value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); } // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, // but it would mean to define eight // (for every problematic property) identical functions if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { style[ name ] = "inherit"; } // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]赋值,否则直接赋值 if ( !hooks || !( "set" in hooks ) || ( value = hooks.set( elem, value, extra ) ) !== undefined ) { // Support: IE // Swallow errors from 'invalid' CSS values (#5509) try { style[ name ] = value; } catch ( e ) {} } /** * ---- 3、elem.style方式 - 取值 ---- */ } else { // 有钩子先使用钩子,看返回值是否为undefined决定是否style[ name ]取值,否则直接取值 if ( hooks && "get" in hooks && ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { return ret; } return style[ name ]; } }, // 默认computedStyle/currentStyle方式只读,也可styles指定读取对象 css: function( elem, name, extra, styles ) { /** * ---- 1、name修正,属性兼容(同style) ---- */ var num, val, hooks, origName = jQuery.camelCase( name ); name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( origName ) || origName ); // 钩子 hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; // 若有钩子,通过钩子读取 if ( hooks && "get" in hooks ) { val = hooks.get( elem, true, extra ); } // 没有钩子,通过封装的curCSS读取 if ( val === undefined ) { val = curCSS( elem, name, styles ); } // 属性值为"normal",若为cssNormalTransform内的属性,把对应值输出 if ( val === "normal" && name in cssNormalTransform ) { val = cssNormalTransform[ name ]; } // extra === "" 去单位处理,若为"normal"、"auto"等字符串,原样返回 // extra === true 强制去单位,若为parseFloat后为NaN的字符串,返回0 // extra === false/undefined 不特殊处理 if ( extra === "" || extra ) { num = parseFloat( val ); return extra === true || isFinite( num ) ? num || 0 : val; } return val; } } );
5、adjsutCSS换算
adjustCSS( elem, prop, valueParts, tween )用于调用jQuery.style对增量计算的换算,并得到最终值。在jq内部,除了css样式会换算,动画处理也支持换算。这里也可以把动画的tween对象的初始值和增量进行累加换算,得到最终值赋给tween对象
难点:
这里需要知道
jQuery.css( elem, prop, "" )通过computedStyle/currentStyle求得的值单位不变,并且被extra=”“去掉了单位。比如初始值是30px,增量为’+=1rem’,先使用增量的单位30rem,然后调用jQuery.css查询跟修改前的初始值比较,比如变成了scale=15倍,则30rem/15=2rem求得原值换算后为2rem,然后再累加返回3rem。
maxIterations设为20有两个原因:1、js浮点误差可能导致两边总是不相等;2、对于首次调整单位变成了很小的倍数趋近于0无法计算,则通过重置为0.5每次乘2直到可以计算,慢慢的调整差距
bug(建议见第4点):
cssNumber列表中属性的值使用无单位增量如’+=10’,而初始值单位为px,将按照初始值单位’+=10px’处理后返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。
比如lineHeight初始值20px,使用’+=4’,变成了赋值24倍
// valueParts为增量匹配结果集,两种形式 // 动画 adjustCSS(tween.elem, prop, rcssNum.exec( value ), tween) // css adjustCSS(elem, prop, rcssNum.exec( value )) function adjustCSS( elem, prop, valueParts, tween ) { var adjusted, // 默认比例 scale = 1, // 最大修正次数 maxIterations = 20, currentValue = tween ? // 动画对象当前属性值计算 function() { return tween.cur(); } : function() { return jQuery.css( elem, prop, "" ); }, // 当前用作累加基数的初始值 initial = currentValue(), // 匹配单位,若不在cssNumber目录,并且没带单位,则当做px unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), // 由于初始值若匹配到单位,都会是px,不是的在执行css过程中jq也有钩子修正,所以有可能需要换算的只有cssNumber列表中项目,或者unit不为px且initial有非0数值的(0无需换算)。初始值为字符串如"auto",则会在下面按照0处理 initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && rcssNum.exec( jQuery.css( elem, prop ) ); // 单位不同时换算 if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { // 默认使用增量值单位 // 若为cssNumber中属性,且增量无单位,则使用初始值单位,后面也无需换算了 // 小bug:cssNumber列表中属性'+=10'若无unit,按照初始值单位'+=10px'处理返回。但返回到外部,由于在cssNumber列表中,并不会再次加上单位,按照倍数被设置了。比如lineHeight初始值20px,使用'+=4',变成了赋值24倍 unit = unit || initialInUnit[ 3 ]; // Make sure we update the tween properties later on valueParts = valueParts || []; // Iteratively approximate from a nonzero starting point // 此处个人觉得没有 || 1 的写法没有必要性,若为0,则无需换算了 initialInUnit = +initial || 1; // 换算,见难点解释 do { // If previous iteration zeroed out, double until we get *something*. // Use string for doubling so we don't accidentally see scale as unchanged below scale = scale || ".5"; // Adjust and apply initialInUnit = initialInUnit / scale; jQuery.style( elem, prop, initialInUnit + unit ); // Update scale, tolerating zero or NaN from tween.cur() // Break the loop if scale is unchanged or perfect, or if we've just had enough. } while ( scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations ); } if ( valueParts ) { // 初始值为字符串,也将按照0处理,需要注意咯 initialInUnit = +initialInUnit || +initial || 0; // 根据是否为增量运算判断直接赋值还是换算后的初始值与增量相加,css运算中只允许增量运算使用该函数 adjusted = valueParts[ 1 ] ? initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : +valueParts[ 2 ]; // 对动画对象赋初值和末值 if ( tween ) { tween.unit = unit; tween.start = initialInUnit; tween.end = adjusted; } } return adjusted; }
6、curCSS、getStyles
curCSS( elem, name, computed )是对
getStyles( elem )的封装,可以通过computed指定样式对象替代内部的getStyle。对高版本浏览器和低版本IE getStyle分别使用的getComputedStyle、currentStyle,前者是全局对象下的属性,所以源码中使用了ownerDocument.defaultView指代。
不同的浏览器,对属性的返回可能出现百分比等非px返回值,jq通过钩子处理个体,curCSS内部也处理了一些情况,比如Chrome、Safari的margin相关属性值返回百分比,低版本IE的非top等位置属性返回百分比等。
// #6489,下面会用到的正则 var rmargin = ( /^margin/ ); var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); // #6692 var getStyles, curCSS, rposition = /^(top|right|bottom|left)$/; // 高版本浏览器 if ( window.getComputedStyle ) { getStyles = function( elem ) { // Support: IE<=11+, 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; // opener指的是打开该页面的源页面的window if ( !view || !view.opener ) { view = window; } return view.getComputedStyle( elem ); }; // 默认使用getStyles,也可通过computed参数指定样式对象。内部还有对文档片段和margin类属性值的特殊处理 curCSS = function( elem, name, computed ) { var width, minWidth, maxWidth, ret, style = elem.style; computed = computed || getStyles( elem ); // getPropertyValue is only needed for .css('filter') in IE9, see #12537 // getComputedStyle(elem).getPropertyValue(name)其实也可以用来获取属性,但是不支持驼峰,必须-连接书写,否则返回"" ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined; // 文档片段document fragments的元素通过getComputedStyle取样式是""或undefined,需要退回到style方式取 // 文档片段中的元素elem.ownerDocument不为文档片段,为docuemnt if ( ( ret === "" || ret === undefined ) && !jQuery.contains( elem.ownerDocument, elem ) ) { ret = jQuery.style( elem, name ); } if ( computed ) { // 为了兼容有的浏览器margin相关方法返回百分比等非px值的情况,由于width输出是px,并且margin的百分比是按照width计算的,因此可以直接赋值width。设置minWidth/maxWidth是为了保证设置的width不会因为超出限制失效 if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) { // 记忆 width = style.width; minWidth = style.minWidth; maxWidth = style.maxWidth; // 把margin的值设置到width,并获取对应width值作为结果 style.minWidth = style.maxWidth = style.width = ret; ret = computed.width; // 还原 style.width = width; style.minWidth = minWidth; style.maxWidth = maxWidth; } } // Support: IE // IE returns zIndex value as an integer.都以字符串返回 return ret === undefined ? ret : ret + ""; }; // IE 6-8 } else if ( documentElement.currentStyle ) { getStyles = function( elem ) { return elem.currentStyle; }; curCSS = function( elem, name, computed ) { var left, rs, rsLeft, ret, style = elem.style; computed = computed || getStyles( elem ); ret = computed ? computed[ name ] : undefined; // Avoid setting ret to empty string here // so we don't default to auto if ( ret == null && style && style[ name ] ) { ret = style[ name ]; } // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels // but not position css attributes, as those are // proportional to the parent element instead // and we can't measure the parent instead because it // might trigger a "stacking dolls" problem // 对非位置top|left|right|bottom返回的,先把left属性保存,然后把属性设置到left上,然后取出 if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { // 记忆 left = style.left; // runtimeStyle是低版本IE中表示运行中样式,可读可写,优先级大于style设置的 rs = elem.runtimeStyle; rsLeft = rs && rs.left; // Put in the new values to get a computed value out if ( rsLeft ) { rs.left = elem.currentStyle.left; } // 对于百分比,是以父元素宽度为基准。而对于fontSize,设置到left的1rem大小则是固定的 style.left = name === "fontSize" ? "1em" : ret; ret = style.pixelLeft + "px"; // 还原 style.left = left; if ( rsLeft ) { rs.left = rsLeft; } } // Support: IE // IE returns zIndex value as an integer.数字以字符串形式返回 return ret === undefined ? ret : ret + "" || "auto"; }; }
7、cssHooks钩子
正如第4点提到的,cssHooks存在的主要目的是应对存取出现的不一致行为。jq用support对象放置测试后的兼容性信息,源码#6519 - #6690行有很多support对象的样式方面兼容测试,内部通过addGetHookIf( conditionFn, hookFn )来绑定钩子。钩子有个computed参数,用于标记是jQuery.css/style哪个方法的读操作触发的,对应true/false
存:存的不一致只有一个
1、opacity透明度对于IE内部使用filter,需要设置为alpha(opacity=100*value)的形式
*、对于padding、borderWidth、height、width的存只是做了负数变为0的特殊处理。
取:取的不一致比较多
1、opacity的”“按”1”处理,对于需要使用filter的IE低版本也要hook
2、height、width的获取需要display不为none和带有table的任意值(除了table、table-cell、table-caption三样),因此提供了
swap( elem, options, callback, args )用于以指定属性状态调用函数取值,之后还原状态
3、marginLeft、marginRight对于不支持返回可靠值的浏览器做处理,marginRight在display为”inline-block”下取值,marginLeft通过getBoundingClientRect比对与marginLeft设置为0后的位置差得到
4、top、left中有可能返回百分比的浏览器,先取值,若不为px单位,则调用内部position方法计算top、left(但是此方法是相对有定位父集或html的,对于position为relative的是有bug的,个人建议源码中可以对relative的使用getBoundingClientRect比对处理)
扩展: innerWidth()/innerHeight()/outerWidth()/outerHeight()
盒模型默认是宽高不包括padding、border、margin。css3里有boxSizing属性,content-box|border-box|inherit分别代表 “不包括padding、border、margin” | “包含border和padding” | “继承”。
jq通过innerWidth()/innerHeight()可以直接查询/设置content-box区域的长宽;通过outerWidth()/outerHeight()可查询/设置为border-box区域的长宽,增加一个参数true,如([value, ]true),可查询/设置为border-box区域加上margin区域的总长宽。
jq仍然是设置height、width,不过它会进行换算。通过
augmentWidthOrHeight( elem, name, extra, isBorderBox, styles )计算增量(数值),通过
getWidthOrHeight( elem, name, extra )得到最终值(带px字符串)。通过
extra来指明按照content、padding、border、margin中哪一个级别。
注意:
cssHooks内若要得到自身属性的样式,不调用jQuery.css,而是直接调用curCSS,包括getWidthOrHeight内,因为curCSS是纯粹的取值,不会调用钩子造成死循环
/* #1307 contains * 节点包含。后面经常用来验证是否为文档片段中的元素 ---------------------------------------------------------------------- */ // /^[^{]+\{\s*\[native \w/ -> 匹配内部方法 'funtion xxx() { [native code] }' hasCompare = rnative.test( docElem.compareDocumentPosition ); // 返回布尔值,true表示 b节点在a节点内/a文档的根节点内(节点相等为false) // ie9+及其他浏览器支持compareDocumentPosition,ie6-8支持contains,比较老的safari都不支持使用下面的函数 contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { var adown = a.nodeType === 9 ? a.documentElement : a, bup = b && b.parentNode; return a === bup || !!( bup && bup.nodeType === 1 && ( adown.contains ? adown.contains( bup ) : // a.compareDocumentPosition( bup ) = 16表示 a包含b a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 )); } : function( a, b ) { if ( b ) { // 若不等于父节点,继续冒泡知道根节点 while ( (b = b.parentNode) ) { if ( b === a ) { return true; } } } return false; }; /* #6494 swap * elem的style在options状态下调用callback.apply(elem, args),然后改回原属性。返回查到的值 ---------------------------------------------------------------------- */ var swap = function( elem, options, callback, args ) { var ret, name, old = {}; // 记录原有值,设定上新值 for ( name in options ) { old[ name ] = elem.style[ name ]; elem.style[ name ] = options[ name ]; } ret = callback.apply( elem, args || [] ); // 还原旧值 for ( name in options ) { elem.style[ name ] = old[ name ]; } return ret; }; /* #6816 addGetHookIf * conditionFn()执行后返回true,说明支持,不会绑定hookFn钩子 ---------------------------------------------------------------------- */ function addGetHookIf( conditionFn, hookFn ) { // Define the hook, we'll check on the first run if it's really needed. // 预执行和懒加载的方式均可,源码选择了懒加载。第一次当做钩子执行调用时绑定真实钩子或删除 return { get: function() { if ( conditionFn() ) { // 支持,无需钩子 delete this.get; return; } // 需要钩子,定义为hookFn。即使是第一次也要执行一次 return ( this.get = hookFn ).apply( this, arguments ); } }; } /* #6935 setPositiveNumber * 保证非负值,保留单位,subtract可以指定需要减去的值 ---------------------------------------------------------------------- */ function setPositiveNumber( elem, value, subtract ) { var matches = rnumsplit.exec( value ); return matches ? // Guard against undefined "subtract", e.g., when used as in cssHooks Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : value; } /* #6944 augmentWidthOrHeight * 根据extra类型计算增量(相对于height/width取值),返回纯数值 * 注意:读取为增量,写入为减量 ---------------------------------------------------------------------- */ function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { // border-box -> border, content-box -> content。无需修正为4 var i = extra === ( isBorderBox ? "border" : "content" ) ? // If we already have the right measurement, avoid augmentation 4 : // height: 0(top) 2(bottom) width: 1(right) 3(left) // cssExpand = [ "Top", "Right", "Bottom", "Left"]; name === "width" ? 1 : 0, val = 0; for ( ; i < 4; i += 2 ) { // border-box content-box 想变为margin级别都需要 + margin值 if ( extra === "margin" ) { val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); } if ( isBorderBox ) { // border-box = content级别 + "padding" + "border"(下面那个) if ( extra === "content" ) { // true 表示强制去单位 val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); } // border-box = padding级别 + "border" if ( extra !== "margin" ) { val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } else { // 逻辑能走到这里,说明一定不是content级别,否则 i = 4 // content-box 变为任意级别都要 + padding 。 true 表示强制去单位 val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); // 变为border级别、margin级别要 + border if ( extra !== "padding" ) { val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } } return val; } /* #6988 getWidthOrHeight * 用于读取,会根据extra加上augmentWidthOrHeight增量 ---------------------------------------------------------------------- */ function getWidthOrHeight( elem, name, extra ) { // getWidthOrHeight = contentBox级别值 + augmentWidthOrHeight增量 // 这里直接用offsetWidth/offsetHeight返回的borderbox级别值作为基础值,因此下面需要调整,valueIsBorderBox默认值为true,表示为border-box var valueIsBorderBox = true, val = name === "width" ? elem.offsetWidth : elem.offsetHeight, styles = getStyles( elem ), // 只有支持boxSizing属性,且为border-box,isBorderBox才为true,否则要调整val isBorderBox = support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; // Support: IE11 only,全屏浏览下bug,不了解(逃 // In IE 11 fullscreen elements inside of an iframe have // 100x too small dimensions (gh-1764). if ( document.msFullscreenElement && window.top !== window ) { // Support: IE11 only // Running getBoundingClientRect on a disconnected node // in IE throws an error. if ( elem.getClientRects().length ) { val = Math.round( elem.getBoundingClientRect()[ name ] * 100 ); } } // some non-html elements return undefined for offsetWidth, so check for null/undefined // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 // svg 和 MathML 可能会返回 undefined,需要重新求值 if ( val <= 0 || val == null ) { // 直接获取 width/height 作为基础值,若之后调用elem.style,说明support.boxSizingReliable()一定为false val = curCSS( elem, name, styles ); if ( val < 0 || val == null ) { val = elem.style[ name ]; } // 匹配到非px且带单位的值,则直接退出 if ( rnumnonpx.test( val ) ) { return val; } // valueIsBorderBox意思是得到的value是borderbox级别的,由于调整为了curCSS取值,因此,必须要isBorderBox为true,不可靠值当做content级别处理(因为border、padding容易获取到准确值,val === elem.style[ name ]除外) valueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] ); // Normalize "", auto, and prepare for extra // 强制去单位,"auto"等字符串变为0 val = parseFloat( val ) || 0; } // use the active box-sizing model to add/subtract irrelevant styles return ( val + augmentWidthOrHeight( elem, name, // 若没指定,默认值跟盒模型一致 extra || ( isBorderBox ? "border" : "content" ), // 表示基数val是否为borderBox,extra和它一致说明无需累加 valueIsBorderBox, styles ) ) + "px"; } /* #7201 cssHooks[ "height", "width" ] * 防止设置负数。支持根据extra指定级别修正设定/获取值 ---------------------------------------------------------------------- */ jQuery.each( [ "height", "width" ], function( i, name ) { jQuery.cssHooks[ name ] = { get: function( elem, computed, extra ) { // innerWidth等API内部只调用jQuery.css,style方式不用钩子,所以false则退出 if ( computed ) { // rdisplayswap = /^(none|table(?!-c[ea]).+)/ // cssShow = { position: "absolute", visibility: "hidden", display: "block" } // display影响了定位信息的获取,比如offsetWidth为0。先设置cssShow属性获取到值,然后改回属性 return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ? swap( elem, cssShow, function() { return getWidthOrHeight( elem, name, extra ); } ) : getWidthOrHeight( elem, name, extra ); } }, set: function( elem, value, extra ) { var styles = extra && getStyles( elem ); // 设置非负值,在设置时增量即为减量,第三个参数对于substract参数 return setPositiveNumber( elem, value, extra ? augmentWidthOrHeight( elem, name, extra, support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", styles ) : 0 ); } }; } ); /* #7053 #7233 cssHooks[ "opacity" ] * 根据是否支持opacity,判断内部使用opacity还是filter ---------------------------------------------------------------------- */ cssHooks: { opacity: { get: function( elem, computed ) { // elem.style['opacity']调用无需钩子,所以false则不处理 if ( computed ) { // We should always get a number back from opacity var ret = curCSS( elem, "opacity" ); return ret === "" ? "1" : ret; } } } }, // #7233 filter部分 if ( !support.opacity ) { jQuery.cssHooks.opacity = { // computed -> css(true)、style(false) 均hook get: function( elem, computed ) { // IE uses filters for opacity // ropacity = /opacity\s*=\s*([^)]*)/i, return ropacity.test( ( computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter ) || "" ) ? ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : computed ? "1" : ""; }, set: function( elem, value ) { var style = elem.style, currentStyle = elem.currentStyle, opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", filter = currentStyle && currentStyle.filter || style.filter || ""; // IE has trouble with opacity if it does not have layout // Force it by setting the zoom level style.zoom = 1; // if setting opacity to 1, and no other filters exist - // attempt to remove filter attribute #6652 // if value === "", then remove inline opacity #12685 if ( ( value >= 1 || value === "" ) && jQuery.trim( filter.replace( ralpha, "" ) ) === "" && style.removeAttribute ) { // Setting style.filter to null, "" & " " still leave "filter:" in the cssText // if "filter:" is present at all, clearType is disabled, we want to avoid this // style.removeAttribute is IE Only, but so apparently is this code path... style.removeAttribute( "filter" ); // if there is no filter style applied in a css rule // or unset inline opacity, we are done if ( value === "" || currentStyle && !currentStyle.filter ) { return; } } // otherwise, set new filter values // ralpha = /alpha\([^)]*\)/i style.filter = ralpha.test( filter ) ? filter.replace( ralpha, opacity ) : filter + " " + opacity; } }; } /* #7282 cssHooks[ "marginRight" "marginLeft" ] * 只对不支持的浏览器使用 get 钩子,通过addGetHookIf ---------------------------------------------------------------------- */ jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, function( elem, computed ) { // 仅css方式 if ( computed ) { return swap( elem, { "display": "inline-block" }, curCSS, [ elem, "marginRight" ] ); } } ); jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, function( elem, computed ) { // 仅css方式 if ( computed ) { return ( parseFloat( curCSS( elem, "marginLeft" ) ) || // Support: IE<=11+ // Running getBoundingClientRect on a disconnected node in IE throws an error // Support: IE8 only // getClientRects() errors on disconnected elems ( jQuery.contains( elem.ownerDocument, elem ) ? // 与marginLeft=0的left坐标比对差值 elem.getBoundingClientRect().left - swap( elem, { marginLeft: 0 }, function() { return elem.getBoundingClientRect().left; } ) : // 文档片段中的元素按0处理 0 ) ) + "px"; } } ); /* #10882 cssHooks[ "top" "left" ] * 只对不支持px返回位置的浏览器使用 get 钩子,通过addGetHookIf ---------------------------------------------------------------------- */ // jQuery.each( [ "top", "left" ], function( i, prop ) { jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, function( elem, computed ) { if ( computed ) { computed = curCSS( elem, prop ); // if curCSS returns percentage, fallback to offset // position是相对最近的有定位的祖先节点的偏移,对于position:relative是不合适的,算是个bug吧 return rnumnonpx.test( computed ) ? jQuery( elem ).position()[ prop ] + "px" : computed; } } ); } );
8、innerWidth/Height()、outerWidth/Height()
第7点已经讲了很多,增量的处理,并不在这几个函数中,统一交给了augmentWidthOrHeight,使得逻辑变得简单统一,很好的实践了分离可变与不变的思想。注意:它们不受jQuery.style中的小bug(如200有效,’200’无效)影响。它们不在cssNumber列表,虽然不会再该函数里为字符串自动补px,但是钩子中的setPositiveNumber是自动补单位px输出的。
// #269 jQuery.isWindow = function( obj ) { /* jshint eqeqeq: false */ return obj != null && obj == obj.window; }; // #10696 function getWindow( elem ) { return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; } // #10893 jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, // funcName代表innerWidth、innerHeight、outerWidth、outerHeight function( defaultExtra, funcName ) { // 参数 ( [ margin [,value] ] ),margin才是数值 // 正确使用 $().outerHeight(值, true) jQuery.fn[ funcName ] = function( margin, value ) { // defaultExtra有值,说明是padding和content的情况,只要有参数说明链式 // defaultExtra为"",说明是margin和border情况,只有首参数有值并不为boolean,则链式 var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), // 确定级别 extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); // 第1类access用法 return access( this, function( elem, type, value ) { var doc; if ( jQuery.isWindow( elem ) ) { // "clientHeight" "clientWidth" 可视区大小 return 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 // unfortunately, this causes bug #3838 in IE6/8 only, // but there is currently no good, small way to fix it. return Math.max( elem.body[ "scroll" + name ], doc[ "scroll" + name ], elem.body[ "offset" + name ], doc[ "offset" + name ], doc[ "client" + name ] ); } // 此处value -> chainable ? margin : undefined return value === undefined ? // extra传递级别,并且不强制的去除单位 jQuery.css( elem, type, extra ) : // Set width or height on the element jQuery.style( elem, type, value, extra ); }, type, chainable ? margin : undefined, chainable, null ); }; } ); } );
9、display与jQuery.fn.show/hide/toggle
与jQuery.fn.css()在一起定义的还有jQuery.fn.show/hide/toggle()方法,通过display:none影响元素的可见性。如同swap函数会记录样式并还原一样,为了让隐藏还原为可见的过程中display与隐藏前保持一致,把display的原状态存入jQuery._data(elem, “olddisplay”)缓存,对于没有缓存的显示的时候会调用defaultDisplay( nodeName )首先查询elemdisplay列表中是否有默认值,若没有,内部调用
actualDisplay( name, doc )通过创建一个相同标签检测display得到需要设置的display值(结果会添加到elemdisplay列表加快以后查询),从而保证正确的显示状态。这一切的核心过程就是
showHide()
showHide()这个函数跟jq中很多函数一样,很污!合并大量分支条件,就是不肯多写两行代码的臭毛病,原谅我年轻,赶脚是在炫技!!
要点:
1、设置为hide,都是通过elem.style.display= none。缓存显示状态到_data(已经有缓存值则不需要)
2、当调整为显示时使用缓存值。缓存值一定是最终可靠值,尽管信任
3、这种逻辑关系建立在只通过showHide()机制控制是否显示,这样记忆的值才能真正代表上一次
show 变量代表变为显示状态,hidden 变量代表本身是隐藏
show(设置显示) -> hidden(本身隐藏) :调整为_data缓存中保存的用于显示的设置;若无缓存,先把display设置为”“,若此时检测仍为hidden,则设置display到元素初始默认,并缓存起来(对文档片段也是友好的)
show(设置显示) -> !hidden(本身显示) :不做处理。下面会路过elem.style.display === “” -> values[ index ] || “” ,相当于没处理
!show(设置隐藏)-> hidden(本身隐藏) :display && display !== “none”,不为”“但仍隐藏,说明这段代码是为了兼容文档片段的,会缓存elem.style.display。最终display设为none
!show(设置隐藏)-> !hidden(本身显示) :缓存css取到的生效样式,因为style的可能为”“,最终display设为none
/* #4304 isHidden * 对display:none,或者文档片段中的节点,返回true ---------------------------------------------------------------------- */ var isHidden = function( elem, el ) { elem = el || elem; return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); }; // #6427 默认display值列表 var iframe, elemdisplay = { // Support: Firefox // We have to pre-define these values for FF (#10227) HTML: "block", BODY: "block" }; /* #6443 actualDisplay * 创建时默认生效的display ---------------------------------------------------------------------- */ function actualDisplay( name, doc ) { var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), display = jQuery.css( elem[ 0 ], "display" ); // #6251 删除该节点 elem.detach(); return display; } /* #6459 defaultDisplay * 创建时默认生效的display ---------------------------------------------------------------------- */ function defaultDisplay( nodeName ) { var doc = document, display = elemdisplay[ nodeName ]; if ( !display ) { display = actualDisplay( nodeName, doc ); // If the simple way fails, read from inside an iframe // 失效了,就在一个iframe再试一次 if ( display === "none" || !display ) { // Use the already-created iframe if possible iframe = ( iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" ) ) .appendTo( doc.documentElement ); // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document; // Support: IE doc.write(); doc.close(); display = actualDisplay( nodeName, doc ); iframe.detach(); } // Store the correct default display,缓存 elemdisplay[ nodeName ] = display; } return display; } /* #6878 showHide * show为true设置显示,为false设置隐藏 ---------------------------------------------------------------------- */ function showHide( elements, show ) { var display, elem, hidden, values = [], index = 0, length = elements.length; for ( ; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } // 缓存值 values[ index ] = jQuery._data( elem, "olddisplay" ); display = elem.style.display; if ( show ) { // display === "none"是无需记忆的属性,可以直接修改来试探 if ( !values[ index ] && display === "none" ) { elem.style.display = ""; } // 显示转隐藏时缓存才必要,这里只是为了方便,因为要被设置为这个值,可以直接借用下面values[ index ] || "" if ( elem.style.display === "" && isHidden( elem ) ) { values[ index ] = jQuery._data( elem, "olddisplay", defaultDisplay( elem.nodeName ) ); } } else { hidden = isHidden( elem ); // !hidden显示转!show隐藏需缓存,文档片段造成的隐藏仍需缓存display && display !== "none" if ( display && display !== "none" || !hidden ) { jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); } } } // 开始设置 for ( index = 0; index < length; index++ ) { elem = elements[ index ]; if ( !elem.style ) { continue; } if ( !show || elem.style.display === "none" || elem.style.display === "" ) { elem.style.display = show ? values[ index ] || "" : "none"; } } return elements; } /* #7362 show hide toggle * 外观,toggle不带参数则是设置为相反。带参数则控制show/hide ---------------------------------------------------------------------- */ jQuery.fn.extend( { show: function() { return showHide( this, true ); }, hide: function() { return showHide( this ); }, toggle: function( state ) { if ( typeof state === "boolean" ) { return state ? this.show() : this.hide(); } return this.each( function() { // 文档片段总是隐藏,调用show if ( isHidden( this ) ) { jQuery( this ).show(); } else { jQuery( this ).hide(); } } ); } } );
定位
jQuery不仅能够便利的支持快捷的盒模型设置,还提供了兼容的方式获取定位信息。jQuery.fn.offset()
无参数时,获取元素到页面左上角的距离,包括滚动条。通常通过getBoundingClientRect()获取相对视口的(对于IE6-8左上角左边从2,2开始算,所以要减去document.documentElement.clientTop/left),然后加上滚动距离window.pageXOffset/pageYOffset(IE低版本不支持,需要用document.documentElement.scrollTop/scrollLeft,这里不考虑IE混杂模式),下面还实现了
jQuery.fn.scrollLeft()/scrollTop()兼容的获取或设置元素或window的滚动距离,源码中没调用这个方法,个人觉得有点奇怪虽然无伤大雅。
支持两种参数形式(css获取的position为”static”会设为”relative”),内部调用
jQuery.offset.setOffset( elem, options, i )设置:
对象{left: x, top: x}直接设置
函数fn(i, options),会注入当前位置信息({left: x, top: x})到options,可在函数内修正坐标,之后options会被设置到元素
jQuery.fn.position()
读取基于最近有定位的祖先节点或根节点的border内侧的相对偏移,margin部分也算做了元素的一部分,所以偏移量不包含margin。之所以选择包含margin,是为了css设置top和left定位元素考虑,这样就不用担心因为margin的影响要修正top和left了
在原生方法中有elem.offsetLeft、elem.offsetTop,代表相对最近有定位祖先的距离,但是源码却没用它,反而用了更麻烦的方法获取到元素和有定位祖先的offset()然后相减,原因应该是因为IE低版本的两个方法有bug,获取的是相对父集的偏移而无需有定位。
/* #10698 show hide toggle * document和window会返回对应的window,其他元素都返回false ---------------------------------------------------------------------- */ function getWindow( elem ) { return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 ? elem.defaultView || elem.parentWindow : false; } /* #10706 jQuery.offset.setOffset * jQuery.fn.offset内用于设置时调用的核心方法,设置top、left属性 ---------------------------------------------------------------------- */ jQuery.offset = { setOffset: function( elem, options, i ) { var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, position = jQuery.css( elem, "position" ), curElem = jQuery( elem ), props = {}; // 需要把默认变为相对定位,设置才能生效。此时top、left是以它当前位置为基准的(与absolute不同) if ( position === "static" ) { elem.style.position = "relative"; } // 获取相对页面起始处的距离(含滚动区域) curOffset = curElem.offset(); // 当前的top、left值 curCSSTop = jQuery.css( elem, "top" ); curCSSLeft = jQuery.css( elem, "left" ); // relative的auto是0,而absolute与fixed的auto并不相当于0,因为它们是相对最近的有定位祖先节点或根元素的 calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray( "auto", [ curCSSTop, curCSSLeft ] ) > -1; // 计算 if ( calculatePosition ) { // 为auto时,获取相对最近的有定位祖先节点或根元素的距离,得到真实值,得到值无单位。因为margin值被考虑在方法中,因此获取的top/left无需修正 curPosition = curElem.position(); curTop = curPosition.top; curLeft = curPosition.left; // relative无需计算,需要去除单位 } else { curTop = parseFloat( curCSSTop ) || 0; curLeft = parseFloat( curCSSLeft ) || 0; } // options为函数时,调用得到修正后需要设置的offset()坐标,并赋值给options if ( jQuery.isFunction( options ) ) { // fn(i, options) 可自定义修正坐标 options = options.call( elem, i, jQuery.extend( {}, curOffset ) ); } // 相对之前offset()增加的值,就是需要在top\left属性上追加的值 if ( options.top != null ) { props.top = ( options.top - curOffset.top ) + curTop; } if ( options.left != null ) { props.left = ( options.left - curOffset.left ) + curLeft; } // 可以通过using属性定义钩子函数,取代默认的写入 if ( "using" in options ) { options.using.call( elem, props ); } else { // 默认直接通过{top: x, left: x}对象形式调用css写入 curElem.css( props ); } } }; jQuery.fn.extend( { /* #10757 jQuery.fn.offset * 相对页面(含滚动区)的坐标对象{left,top},可读可写,参数可为函数 ---------------------------------------------------------------------- */ offset: function( options ) { if ( arguments.length ) { return options === undefined ? this : // 对所有元素设置 this.each( function( i ) { jQuery.offset.setOffset( this, options, i ); } ); } var docElem, win, box = { top: 0, left: 0 }, elem = this[ 0 ], doc = elem && elem.ownerDocument; if ( !doc ) { return; } docElem = doc.documentElement; // 文档片段元素按照{left:0, top:0}返回 if ( !jQuery.contains( docElem, elem ) ) { return box; } // If we don't have gBCR, just use 0,0 rather than error // BlackBerry 5, iOS 3 (original iPhone) if ( typeof elem.getBoundingClientRect !== "undefined" ) { // 得到相对视口的坐标 box = elem.getBoundingClientRect(); } win = getWindow( doc ); return { // + 滚动距离 - 低版本IE的(2,2)修正 top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) }; }, /* #10794 jQuery.fn.position * 相对最近的有定位祖先的位置,margin范围算作元素的一部分 ---------------------------------------------------------------------- */ position: function() { if ( !this[ 0 ] ) { return; } var offsetParent, offset, parentOffset = { top: 0, left: 0 }, elem = this[ 0 ]; // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, // because it is its only offset parent if ( jQuery.css( elem, "position" ) === "fixed" ) { // fixed的top和left是以视口为基准,直接取坐标 offset = elem.getBoundingClientRect(); } else { // Get *real* offsetParent // offsetParent()方法会把冒泡到body节点的(且body也无定位)按照html节点处理 offsetParent = this.offsetParent(); // Get correct offsets // 得到自身和offsetParent的offset() offset = this.offset(); if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { parentOffset = offsetParent.offset(); } // Add offsetParent borders // Subtract offsetParent scroll positions // 修正,因为是到offsetParent的border内侧 //如果元素本身带滚动条,并且滚动了一段距离,那么两者间实际的偏移应该更多 parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ) - offsetParent.scrollTop(); parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ) - offsetParent.scrollLeft(); } // 两offset()相减,margin需作为元素一部分,去掉 return { top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true ) }; }, /* #10837 jQuery.fn.offsetParent * 获得元素集合对应的offsetParent(或html元素)集合 ---------------------------------------------------------------------- */ offsetParent: function() { return this.map( function() { // 最近的有定位的上层元素 var offsetParent = this.offsetParent; // display:none或position:fixed为null。正常元素若找不到则向上到body返回 while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { offsetParent = offsetParent.offsetParent; } return offsetParent || documentElement; } ); } } ); /* #10851 jQuery.fn.scrollLeft/scrollTop * 滚动距离 ---------------------------------------------------------------------- */ jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { var top = /Y/.test( prop ); jQuery.fn[ method ] = function( val ) { return access( this, function( elem, method, val ) { // window有单独的推荐方法,所以先识别是否为window或document var win = getWindow( elem ); if ( val === undefined ) { // window,并且兼容pageXOffset,则使用推荐的window.pageXOffset // 否则使用元素的scrollLeft/scrollTop方法,window则使用根元素的该方法 return win ? ( prop in win ) ? win[ prop ] : win.document.documentElement[ method ] : elem[ method ]; } // window.scrollTo(xpos,ypos),对于不需改变的那个使用原值jQuery( win ).scrollxxx() if ( win ) { win.scrollTo( !top ? val : jQuery( win ).scrollLeft(), top ? val : jQuery( win ).scrollTop() ); } else { // elem.scrollLeft/scrollTop = xxx; elem[ method ] = val; } }, method, val, arguments.length, null ); }; } );
相关文章推荐
- 锋利的jQuery第2版学习笔记8~11章
- 锋利的jQuery第2版学习笔记6、7章
- Dojo和jQuery区别
- a毛 jquery 学习记 3 常规选择器(2)
- 锋利的jQuery-第三章 jQuery中的DOM操作
- jquery css快捷方法
- jquery 设置元素内容html(),text(),val()
- jQuery对象常用方法
- Javascript,Jquery实现页面图片预加载百分比展现
- jQuery
- 玉渊潭赏樱花有感:从无到有写一个jQuery开源插件
- jQuery选择器总结
- Jquery-select元素操作方法
- jQuery $.each用法
- JavaScript之Ajax-6 Ajax的增强操作(jQuery对Ajax的支持、表单操作)
- 10分钟-jQuery与Ajax
- Jquery操作XML
- jQuery- v1.10.2 源码解读
- jquery 相关class属性的操作
- jQuery学习笔记之unbind()