jQuery 源码:操作样式

一、概述

本文围绕着 jquery 如何对 css 属性进行读取和设置这个主题,以 MDN/MSDN 文档,以及其它优秀的博文作为参考,对 jquery 1.6.3 中相关的关键性兼容问题做出分析。

本文中出现的重要概念有 initial value , specified value , computed value , used value , actual value。重要函数和变量有: getComputedStyle , currentStyle , style , pixelLeft . 

二、Initial , specified , computed, used and actual value

按照CSS2标准的解说,浏览器一旦创建了文档树,则它必须为每个元素的每个属性赋一个值。最终值是由以下四步计算得到:1)获得 specified value 2)把 specified value解析成可以用于继承的 computed value 3)如果需要,把 computed value 转化成 used value 4)把 used value 转化成适应本地环境的 actual value。

1. initial value

元素的 css 属性的 initial value 该 css 属性的属性,存在于 css 属性的定义中。对于任意元素的非继承性 css 属性,只要不对该属性显式赋值(通过样式表/js等),则该属性值取值为 initial value;对于继承性 css 属性,并且属性未被赋值,则根元素会使用 initial value,其它的则按照继承规则继承自祖先节点。

下面是一些例子:

属性名 继承性 initial value 哪些元素拥有此属性 其它
color y 浏览器决定 all elements  
clear n none block-level elements (including floats)  
padding-bottom n 0 all elements except those with a display value of table-row-grouptable-header-grouptable-footer-grouptable-rowtable-column-group and table-column 当为百分数时,相对于最近的块级祖先元素的width计算
height n auto block level and replaced elements 当为百分数时,相对于父级块元素的height计算

2. specified value

虽然 CSS2 标准未给出 specified 的定义,但是给出了获取 specified value 的过程:

1)通过 author style, user style , user agent style 级联得到属性值,如果存在,则使用该值

2)否则,如果该属性是继承属性并且元素不是文档根元素,则使用父元素的 computed style 。(注: 可以看出 computed value 的一个作用是用于样式的继承)

3)否则,使用元素的 initial value ,initial value 是在元素的定义中定义的。

顺便谈一谈 currentStyle

IE5 开始支持元素的 currentStyle 对象,据说从 currentStyle 获取的属性值就是 specified value ,但从 MSDN 的定义来看,只能说像。

MSDN 给出的定义很难读:

"Represents the cascaded format and style of the object as specified by global style sheets, inline styles, and HTML attributes"

不过,评论部分提供的获取 currentStyle 的计算规则就比较有用了:

The currentStyle object reflects the order of style precedence in CSS. The CSS order of precedence for the presentation of HTML is:

  1. Inline styles
  2. Style sheet rules
  3. Attributes on HTML tags
  4. Intrinsic definition of the HTML tag(笔者注: HTML 标签的特性定义)

这几句话指出 currentStyle 是由上述 4 中因素级联得到的。其中,1,2,3 应该是上文提到的 Author style,4 则像是 initial value。由此看来,currentStyle 相比于 specified value 缺少了 user style , user agent style , computed style 这几个因素。应该是有区别的。

本文关注 currentStyle 的最重要的一个特点是:

它获取的长度、大小相关的值,可能是 '10mm' , '1%' , '1em' 等形式,这与 computed style/used value有很大区别。

举几个例子来了解一下 currentStyle:

1)返回以 'mm' 为单位的 fontSize ,返回百分数的 width

通过样式表或js指定元素的 font-size: 10mm,则 currentStyle.fontSize 为 '10mm' ;指定 50% , 则 currentStyle.width 为 50% 。

2)设置 style 对象或者 runtimeStyle 对象,会即时地反映到 currentStyle 对象上

本文第三部分会详解二者之间的关系

3. computed value

computed value 在级联产生 specified value 的过程中生成,'em' 、'ex' 这些单位被转换为 'px',不要求文档渲染完成才能获取 computed value (这句话参考自 css2 标准)。

"Specified values are resolved to computed values during the cascade; for example URIs are made absolute and 'em' and 'ex' units are computed to pixel or absolute lengths. Computing a value never requires the user agent to render the document."

但是,computed value 也可能是个百分数。例如,若 width 的 specified value 是 10%,而此时文档还没有渲染好,我们无法知道父元素的实际像素宽度,从而 computed value 为 10% 。(参考自 MDN computed value)。

"However, for some properties (those where percentages are relative to something that may require layout to determine, such as width, margin-right, text-indent, and top), percentage specified values turn into percentage computed values. Additionally, unitless numbers specified on the line-height property become the computed value, as specified. These relative values that remain in the computed value become absolute when the used value is determined."

了解这一点,对于搞懂 used value 非常重要。

4. used value

有些属性,例如 3. computed value 部分所讲的 width,它的 computed value 得到了 10% , 当父元素渲染完毕,我们就能把这个 computed value 转换为以 'px' 为单位的绝对长度了,转换得到的值就是 used value。

document.defaultView.getComputedValue() 函数返回的是 used value ,而不是 computed value ,这一点要切记。

因为 used value 必须等到元素渲染完毕才能得到,所以,如果元素不在文档中(说明还没渲染),这意味着通过 getComputedStyle() 获取的属性将是空串。JQ 面对这种情况时,只好利用 style 返回内联样式值。

5. actual value

actual value 是把 used value 做了近似计算之后得到的值。处于渲染精度或者其它原因,浏览器需要做这种近似计算。

三、style , runtimeStyle and currentStyle

runtimeStyle 是 IE 的专有属性,与 style 对象一样,都可以用于设置样式并能即时地影响 currentStyle,runtimeStyle 具备更高的优先级;通过 runtimeStyle 获取的 css 属性值为空串,除非显式地通过 runtimeStyle 设置了属性值;设置 runtimeStyle 不影响 style 的取值,二者是独立的;runtimeStyle 对样式的影响不是永久的,把属性值重置为空串就能取消 runtimeStyle 对该属性的影响。

JQ 如何使用 currentStyle :

IE9 之前的版本不支持 getComputedStyle() 函数,对于没有这个函数的浏览器,JQ 使用 currentStyle 来做兼容处理。但是,在之前的论述中我们提到过,getComputedStyle() 获取的属性值是 uesed value,currentStyle 获取的属性值更像是 specified value。两者最直观的区别是,specified value 可能会出现相对单位('1em' , '1%' 等)以及非 'px' 的单位('mm' , 'pt'等)。JQ 的解决办法是,对从currentStyle 获取的值进行单位转换,获得以'px'为单位的绝对长度。

JQ 让浏览器帮忙做单位转换:

style 对象里有几个特殊的属性: pixelLeft , pixelTop , pixelWidth , pixelHeight 等,这些变量属于 javascript 的 number 类型(typeof elem.style.pixelLeft === 'number'),它们分别表示内联样式的 left , top , width , height 值。MSDN 上说,pixelLeft 反映了 css 的 top 属性,这个概念同样模糊不清。实践表明,这些值默认为 0 ,并且只受内联样式影响(只能通过 style 来影响)。在这里要强调的一点是: runtimeStyle 也不影响它们的取值。这为 JQ 提供了方便之门:

JQ 单位转换的原理是,从currentStyle获取值,如果需要转换,则将这个值赋值给 elem.style.left,然后获取 elem.style.pixelLeft 即为所求(完成之后需要恢复 elem.style.left 原来的值)。同时,注意到style.left 在这个过程中被改变并恢复,容易在 IE 下造成闪烁,为了避免这个问题,JQ 首先把 runtimeStyle.left 设置为 currentStyle.left ,由于 runtimeStyle 优先级高,所以,不论如何改变 style.left ,页面都不改变。当然,最终,JQ 会把 runtimeStyle.left 恢复为原值。

在 IE 下简单模拟获取值并单位转换:

 1     //仅适用于 IE
 2     function topx(elem , propName)
 3     {
 4         var curPropValue = elem.currentStyle[propName],
 5             ret;
 6 
 7         //保存 runtimeStyle.left 和 style.left 的旧值
 8         var oldRuntimeLeft = elem.runtimeStyle.left,
 9             oldStyleLeft = elem.style.left;
10 
11         //改变 runtimeStyle.left 和 style.left
12         elem.runtimeStyle.left = curPropValue;
13         elem.style.left = curPropValue;
14 
15         //获取 经过转换的以 'px' 为单位的绝对长度
16         ret = elem.style.pixelLeft;
17 
18         //回滚 runtimeStyle.left 和 style.left
19         elem.runtimeStyle.left = oldRuntimeLeft;
20         elem.style.left = oldStyleLeft;
21 
22         return ret + 'px';
23     }

 上述函数基本反映了我们的转换方法,不过还存在一些兼容问题,其中之一就是 font-size ,如果设置 font-size 为 50% 时,利用我们这个方法获取的值将是父元素宽度的 50% ,而不是父元素 font-size 的 50%;如果设置 font-size 为 '2em' 则我们的方法返回父元素 font-size 的四倍。以下的页面说明了这个问题:

View Code
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
    #box{
        background: green;
        width: 200px;
        font-size: 12px;
    }
    #p0{
        width: 50%;
    }
    #p1{
        font-size: 2em;
    }
    #p2{
        font-size: 50%;
    }
    #p3{
        font-size: 20mm;
    }
    </style>
</head>
<body style="position: relative;">
    <div id="box">
        <p id="p0">第零段</p>
        <p id="p1">第一段</p>
        <p id="p2">第二段</p>
        <p id="p3">第三段</p>
        <p id="p4">第四段</p>
    </div>

    <script>

    //仅适用于 IE
    function topx(elem , propName)
    {
        var curPropValue = elem.currentStyle[propName],
            ret;

        //保存 runtimeStyle.left 和 style.left 的旧值
        var oldRuntimeLeft = elem.runtimeStyle.left,
            oldStyleLeft = elem.style.left;

        //改变 runtimeStyle.left 和 style.left
        elem.runtimeStyle.left = curPropValue;
        elem.style.left = curPropValue;

        //获取 经过转换的以 'px' 为单位的绝对长度
        ret = elem.style.pixelLeft;

        //回滚 runtimeStyle.left 和 style.left
        elem.runtimeStyle.left = oldRuntimeLeft;
        elem.style.left = oldStyleLeft;

        return ret + 'px';
    }
    window.onload = function(){
        var box = document.getElementById('box'),
            p0 = document.getElementById('p0'),
            p1 = document.getElementById('p1'),
            p2 = document.getElementById('p2'),
            p3 = document.getElementById('p3');


        alert('p0 ' + p0.currentStyle.width);            //50%
        alert('p0 ' + topx(p0 , 'width'));            //100px

        alert('p1 fontSize:' + p1.currentStyle.fontSize);    //2em
        alert('p1 fontSize:' + topx(p1 , 'fontSize'));        //24px

        alert('p2 fontSize:' + p2.currentStyle.fontSize);    //50%
        alert('p2 fontSize:' + topx(p2 , 'fontSize'));        //100px,不符合预期,期望是 8px

        alert('p3 fontSize:' + p3.currentStyle.fontSize);    //20mm
        alert('p3 fontSize:' + topx(p3 , 'fontSize'));        //
    };

    </script>
</body>
</html>

据此,把第13行代码做如下修改可以满足 fontSize 的特殊性,这就是 JQ 的做法:

        elem.style.left = propName === 'fontSize' ? '1em' : curPropValue;

最后瞟一眼 JQ 1.6.3 代码:

下面这段代码中有两个正则表达式,rnumpx 匹配前有数字后有'px'的字符串(例如 -10px , -1px, 2px , 02px),rnum匹配以数字(-9到9,以及-0)打头的字符串。

    rnumpx = /^-?\d+(?:px)?$/i,
    rnum = /^-?\d/,
View Code
if ( document.documentElement.currentStyle ) {
    currentStyle = function( elem, name ) {
        var left,
            ret = elem.currentStyle && elem.currentStyle[ name ],
            rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ],
            style = elem.style;

        // 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
        if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
            // Remember the original values
            left = style.left;

            // Put in the new values to get a computed value out
            if ( rsLeft ) {
                elem.runtimeStyle.left = elem.currentStyle.left;
            }
            style.left = name === "fontSize" ? "1em" : (ret || 0);
            ret = style.pixelLeft + "px";

            // Revert the changed values
            style.left = left;
            if ( rsLeft ) {
                elem.runtimeStyle.left = rsLeft;
            }
        }

        return ret === "" ? "auto" : ret;
    };
}

凡是以数字打头,又不以'px'结尾的值,都会被转换。

我所无法解释的:

为什么 JQ 会有条件地保存 currentStyle.left 呢? 我已经将这个问题抛在 JQUERY 论坛,欢迎指教。

四. 特殊处理

opacity

IE8 及更早版本不支持 opacity 属性,需要设置 filter: alpha(opacity=XXX); 来兼容。在这些浏览器中能获取到 currentStyle.opacity ,但这仅仅是因为浏览器把opacity当成了自定义属性并且currentStyle支持自定义属性。所以,不要被currentStyle.opacity所迷惑。在低版本的 IE 中,JQ 分别通过currentStyle.filter和style.filter来获取和设置opacity。此外,JQ 设置opacity时,设置了 style.zoom 为1,使元素获得layout,避面IE下内联元素opacity设置无效的问题。

marginRight

2008年的一个jquery bug 里提到,webkit内核的浏览器(safari/chrome)获取marginRight会有兼容问题,当内联元素设置为display:block时,为了使元素占据一行,浏览器自动设置了元素的margin-right,从而导致兼容问题。笔者未测试safari,在当下最新chrome(24.0.1312.52 m)下未发现此BUG。

width/height

通过 $elem.css('width/height') 获取元素的宽高时,也是基于getComputeStyle()函数或者currentStyle变量。不过,假如元素的offsetWidth 或 offsetHeight为0 ,JQ 做了一些额外的工作,先临时设置元素的position:absolute; visibility:visible; display:block; 然后再取值。

参考文章

[1] Computed vs Cascaded Style jquery 使用的 hack 的出处,其中提到的 'cascade style' 在 MSDN/MDN 上都找不到,估计是自己编造的名词。

[2] 关于HTML Object中三个Style实例的区别 博客,你将知道读写 runtimeStyle 造成的影响。重要!

[3] Length unit conversion in javascript ,作者给出了一个比jquery兼容性更好的解决方案,当然作者也提到了jquery,并解释了jquery的做法,很有帮助的文章。

[4] stack overflow 的帖子 有人对 getCurrentStyle() 持有疑问;回复中出现一个链接,有人对 computedStyle ie hack 做了测试 地址

[5] 关于使用runtimeStyle属性问题讨论 一个老江湖使用 runtimeStyle 的经验

原文地址:https://www.cnblogs.com/yaozhiyi/p/2868733.html