jQuery-1.9.1源碼分析系列(七) 鈎子(hooks)機制及瀏覽器兼容


  處理瀏覽器兼容問題實際上不是jQuery的精髓,畢竟讓技術員想方設法取彌補瀏覽器的過錯從而使得代碼亂七八糟不是個好事。一些特殊情況的處理,完全實在浪費瀏覽器的性能;突兀的兼容解決使得的代碼看起來既不美觀也也不能對前端技術有任何提升。但是不管怎么說,只要不同的瀏覽器存在,就有可能出現兼容性問題,我們還必須去解決。比較好的是jQuery提供了一些比較優雅的瀏覽器兼容方案。

  在處理瀏覽器兼容問題的時候最沒有技術含量的方式是if…else..分支判斷。jQuery中用到很多處理兼容的方法:多用於普通兼容性處理的正則表達式處理以及使用的最多的方法是hooks機制。

  

  分析之前先列一下jQuery中整理出來的兼容問題jQuery.support

 

1. jQuery.support詳解


  在jQuery之初就會對瀏覽器檢測查看瀏覽器的支持情況,並將檢測結果保存在jQuery.support中以備后用。可以說,jQuery.support是瀏覽器差別的輪廓(其中<ie8的兼容問題不再考慮)。

jQuery.support = {
  //當使用.innerHTML的時候,IE吞掉開頭的空白。
  leadingWhitespace:  true/false(空白還在/空白被吞掉(IE)), 

  //IE瀏覽器會自動給空表插入tbody標簽。
  tbody:  true/false(tbody可用/tbody會被自動插入(IE)),
  
  
// IE6-8下確保link/ script/ style或其他html5標簽元素能使用innerHTML正確載入的前提是需要一個元素包裹他們。使用div元素來包裹,並且div之前要一個不換行的字符。例如“X<div><link/></div>”。   htmlSerialize: true/false(不用包裹能正確加載/需要包裹(IE)),   
  
//獲取節點的style屬性時:現代瀏覽器使用elem.getAttribute("style"),而IE使用elem.style.cssText   style: true/fasle(使用. getAttribute/IE下使用.cssTex),   
  
//確保節點的css特征opacity存在 (IE使用filter)   opacity: true/false(opacity存在/opacity不存在),   
  
//驗證float樣式存在, (IE使用styleFloat而不是cssFloat)   cssFloat: true/fasle(float樣式使用cssFloat特征名/使用styleFloat特征名),   
  
//檢查checkbox/radio的默認值(老版本WebKit 默認為"",其他瀏覽器默認為"on")   checkOn: true/false(默認值為on/默認值為空),   
  
//確保一個默認選項有一個可用的selected特征值.   //(如果他是一個option組,WebKit的默認選項的selected特征值為false,IE也是。)   optSelected: true/false(有默認選中/沒有默認選中,兼容處理時需要設置一個),      //確保克隆的html5節點(沒有內容)不會出現問題。   //比如document.createElement("nav").cloneNode( true ).outerHTML應該得到"<nav></nav>",而IE卻是"<:nav></:nav>"   html5Clone: true/false(能夠正常克隆/IE下使用cloneNode有問題),      //確保節點的checked狀態也能被克隆   noCloneChecked: true/false(能夠正常克隆/IE下克隆checked狀態沒有被克隆),      //確保option選項disabled而select不被標記為disabled(WebKit會把兩者都標記為disabled)   optDisabled: true/false(disabled正常/兩者都會標記為disabled)      //測試是否能使用delete div.test的方式來刪除特征,否則使用delete div[test](IE<9)   deleteExpando: true/false(可以使用delete div.test/不能使用delete div.test)      //檢查input標簽是否可以使用getAttribute("value")來獲取值(IE下不行,需要使用elem[‘value’])   input: true/false(能信任getAttribute("value")/不能信任getAttribute("value"))      //檢查一個input標簽在更改為radio類型后他的值是否還是先前的值(ie會變成默認值”on”,其他瀏覽器不變)   radioValue: true/false(值不會改變/會改變成默認值),      // webkit不能正確克隆fragments中的checked狀態   checkClone: true/false(能正確克隆/不能正確克隆)      //判斷事件是否被克隆。(兼容IE<9。 Opera不克隆事件(並且typeof div.attachEvent === undefined). IE9-10克隆事件通過attachEvent,但是不能通過 .click()來觸發)   noCloneEvent: true/false(現代瀏覽器克隆節點時事件不被克隆/ie8瀏覽器克隆節點的時候事件也被克隆)      // IE<9 (缺少submit/change事件冒泡),Firefox 17+ (缺少focusin事件)   submitBubbles: true/false(支持冒泡/不支持冒泡),   changeBubbles: true/false(支持冒泡/不支持冒泡),   focusinBubbles: true/false(支持冒泡/不支持冒泡),      //檢查是否能准確克隆css樣式,比如一些可以繼承父節點的樣式沒有設值的時候值應該是inherit,但是並非每個瀏覽器都能獲取到該值。   clearCloneStyle:true/false(能准確克隆/不能准確克隆),      //(判斷前提條件:DOM加載完成)判斷offsetWidth/Height是否可靠(主要用於判斷元素是否占用空間,如果元素占用空間了,就認為是可見的,否則就認為是不可見:hidden);IE8下表格的空cell依然有offsetWidth/Height。   reliableHiddenOffsets: true/false(【offsetWidth/Height值可靠】/【offsetWidth/Height值不可靠】)      //(判斷前提條件:DOM加載完成)測試getComputedStyle獲取的位置是否是像素單位。webkit的bug,使用getComputedStyle返回的最終樣式中top/left/right/bottom不一定是像素為單位的,可能是指定的百分比等   pixelPosition: true/false(位置css樣式以像素為單位返回/位置css樣式以指定的格式返回)      //(判斷前提條件:DOM加載完成)測試設置的boxSizing是否可靠。使用getComputedStyle獲取的最總計算樣式的瀏覽器可能會出現問題。   boxSizingReliable: true/false(可靠/不可靠)      //(判斷前提條件:DOM加載完成)檢測使用getComputedStyle 返回的margin-right值是否正確:WebKit Bug 13343 – getComputedStyle返回錯誤的margin-right值。解決辦法:處理元素的display臨時設置為inline-block的解決來計算。   reliableMarginRight: true/false(返回值可靠/返回值不可靠) } //還有一部分沒有在jQuery.support中,但是在Sizzle引擎中有描述 support = {   //IE8下對節點的一些沒有存在的屬性(attributes)獲取值返回一個字符串   attributes: true/false(返回正確/返回字符串)   
  
//檢測getElementsByClassName是否可靠。IE8不支持;Opera中如果同一個標簽有多個classname,那么他只能找到第一個classname (在 9.6版本中);Safari 3.2會緩存class屬性並且使用. className修改后不會更改緩存。   getByClassName: true/false(可以信賴/不值得信賴)   
  
//檢測getElementsByName是否可靠。IE下某些標簽是沒有name屬性的,比如div。   getByName: true/false(可靠/不可靠)      //檢測瀏覽器是否支持querySelectorAll方法。這里說一個關於context.querySelector/querySelectorAll的要點。context.querySelector查找的是context下匹配的第一個子元素。但是有一個特點就是選擇器可以從context本身開始。比如有一個div如<div id=’chua’ class=’chua’><p></p></div>。我們查找p可以使用document.getElementById('chua') .querySelector('p ')。頁可以是document.getElementById('chua') .querySelector('.chua p')。當然document.getElementById('chua') .querySelector('.chua')是查不到值的。   qsa: ture/false(支持/不支持)      //檢測對matchesSelector的支持情況。目前除IE6-IE8,Firefox/Chrome/Safari/Opera/IE 的最新版本均已實現,但方法都帶上了各自的前綴   matchesSelector: ture/false(支持/不支持)   }

  知識小點:1.elem.getAttribute("href"/”src”)都是寫入什么返回什么,elem.href/elem.src都是返回絕對路徑

  

2. 正則表達式處理兼容


  我們以不同瀏覽器的駝峰寫法不同為例。jQuery.camelCase(string)將string轉化成相應的駝峰寫法。查看源碼

       camelCase: function( string ) {

              return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );

       }

       其中rmsPrefix = /^-ms-/;rdashAlpha = /-([\da-z])/gi; fcamelCase = function( all, letter ) {

return letter.toUpperCase();}

       代碼比較容易理解:對於’-ms-xx’類的字串先轉化成’ms-xx’,然后將’-’后面的第一個字母轉化為大寫變成’msXx’。很明顯使用正則比通過if…else方法來判斷要節省很多代碼量。

  replace函數的說明

  stringObject.replace(regexp/substr,replacement)

  字符串 stringObject 的 replace() 方法執行的是查找並替換的操作。它將在 stringObject 中查找與 regexp 相匹配的子字符串,然后用 replacement 來替換這些子串。如果 regexp 具有全局標志 g,那么 replace() 方法將替換所有匹配的子串。否則,它只替換第一個匹配子串。

  replacement 可以是字符串,也可以是函數。如果它是字符串,那么每個匹配都將由字符串替換。但是 replacement 中的 $ 字符具有特定的含義。如下表所示,它說明從模式匹配得到的字符串將用於替換。

字符

替換文本

$1、$2、...、$99

與 regexp 中的第 1 到第 99 個子表達式相匹配的文本。

$&

與 regexp 相匹配的子串。

$`

位於匹配子串左側的文本。

$'

位於匹配子串右側的文本。

$$

直接量符號。

  注意:ECMAScript v3 規定,replace() 方法的參數 replacement 可以是函數而不是字符串。在這種情況下,每個匹配都調用該函數,它返回的字符串將作為替換文本使用。該函數的第一個參數是匹配模式的字符串(例如‘-webkit-tdd’使用camelCase,則第一次all為-w,第二次為-t)。接下來的參數是與模式中的子表達式匹配的字符串(例如‘-webkit-tdd’使用camelCase,則第一次letter為w,第二次為t),可以有 0 個或多個這樣的參數。接下來的參數是一個整數,聲明了匹配在 stringObject 中出現的位置。最后一個參數是 stringObject 本身。

 

 

3. 鈎子(hooks)機制


  鈎子機制是jQuery用來處理瀏覽器兼容的手法。鈎子在.attr(), .prop(), .val() and .css() 四種操作中會涉及。

  鈎子機制是怎么樣的?

  我們將以一個屬性(attribute)鈎子來舉例。IE9-瀏覽器中,將input標簽更改類型(type)為radio類型以后,value屬性可能出現異常。所以我們定義了一個屬性鈎子(attrHooks)中類型(type)在更改設置(set)的一個處理。結構如下  

//屬性鈎子對象(所有的屬性鈎子都放在里面)
attrHooks: {
  //屬性為type的鈎子   type: {
    //操作為set的鈎子     set:
function( elem, value ) {       if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {         //IE6-9設置完type后恢復value屬性(attr)         var val = elem.value;         elem.setAttribute( "type", value );         if ( val ) { elem.value = val; }           return value;         }       }     }   }
}

  后續的鈎子結構都是這樣的:鈎子對象:{鈎子類型:{鈎子操作:xxx},……}

  鈎子結構我們就清楚了。然后我們來看看jQuery如何使用這些鈎子。只看與鈎子例子相關的部分

//先獲取鈎子,此時name="type"  
hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );   
//此時value="radio"
if
( value !== undefined ) {
  ...
  
} else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {     return ret;   } else {     ...   } }

  使用流程也比較清晰,先獲取指定類型("type")的鈎子(hooks)對象,然后判斷如果鈎子操作("set")在鈎子對象中,則執行之。

  可以想象,任何標簽屬性的任何類型的操作需要做兼容就都可以放在鈎子對象中,如果是新的沒有出現過的新操作則在實現的時候添加一行對新操作的判斷語句處理即可;絕大多數情況是不會出現新操作兼容的,執行添加一個新的鈎子對象的元素即可。可以說拓展性非常好。

  

  接下來一一分析各種鈎子,順便了解相關的瀏覽器的兼容問題。

a. 屬性操作的鈎子


  屬性鈎子種類:

  propFix

  propHooks

  attrHooks

  valHooks

jQuery.propFix

propFix: {
        tabindex: "tabIndex",
        readonly: "readOnly",
        "for": "htmlFor",
        "class": "className",
        maxlength: "maxLength",
        cellspacing: "cellSpacing",
        cellpadding: "cellPadding",
        rowspan: "rowSpan",
        colspan: "colSpan",
        usemap: "useMap",
        frameborder: "frameBorder",
        contenteditable: "contentEditable"
}
  propFix對屬性名稱做了駝峰修正(修正為瀏覽器所支持的標簽屬性),即使用戶大小寫輸入錯誤也能得到修正。
  
需要特別提示的是由於class屬於JavaScript保留值,因此當我們要操作元素的class屬性(attribute)值時,直接使用obj.getAttribute('class')和obj.setAttribute('class', 'value')可能會遭遇瀏覽器兼容性問題,W3C DOM標准為每個節點提供了一個可讀寫的className屬性(attribute),作為節點class屬性的映射,標准瀏覽器的都提供了這一屬性的支持,因此,可以使用e.className訪問元素的class屬性值,也可對該屬性進行重新斌值。而IE和Opera中也可使用e.getAttribute('className')和e.setAttribute('className', 'value')訪問及修改class屬性值。相比之下,e.className是W3C DOM標准,仍然是兼容性最強的解決辦法。、
  
同理htmlFor用於讀取label標簽的for屬性

jQuery.propHooks特征(property)方法

propHooks: {
  tabIndex: {
    get: function( elem ) {
      // elem.tabIndex在沒有明確設置的情況下並不一定能返回正確值                                  
      // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
      var attributeNode = elem.getAttributeNode("tabindex");       return attributeNode && attributeNode.specified ? parseInt( attributeNode.value, 10 ) : rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? 0 : undefined; } } } //其中 //rfocusable = /^(?:input|select|textarea|button|object)$/i, //rclickable = /^(?:a|area)$/i, // Safari 錯誤報告一個選項的默認選中狀態 // 通過父節點的 selectedIndex特征(property)修正他 if ( !jQuery.support.optSelected ) {   jQuery.propHooks.selected =     jQuery.extend( jQuery.propHooks.selected, {       get: function( elem ) {         var parent = elem.parentNode;         if ( parent ) {           parent.selectedIndex;           // 確保他依然適用於option組,詳見 #5701           if ( parent.parentNode ) {             parent.parentNode.selectedIndex;           }         }         return null;       }     }); }

jQuery.attrHooks 方法

attrHooks: {
  type: {
    set: function( elem, value ) {
      if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
        //IE6-9設置完type后恢復value屬性(attr)
        ...
      }
    }
  },
 

//修正老版本IE value屬性(attr)獲取和設置 fix oldIE value attroperty
if ( !getSetInput || !getSetAttribute ) {
  jQuery.attrHooks.value = {
    get: function( elem, name ) {
      var ret = elem.getAttributeNode( name );
      return jQuery.nodeName( elem, "input" ) ?
      //input返回defaultValue,而非特征(property)
      elem.defaultValue :
      ret && ret.specified ? ret.value : undefined;
    },
    set: function( elem, value, name ) {
      if ( jQuery.nodeName( elem, "input" ) ) {
        //input設置defaultValue
        elem.defaultValue = value;
      } else {
        //使用nodeHook,否則將有誤
        return nodeHook && nodeHook.set( elem, value, name );
      }
    }
  };
}

拓展

//其中nodeName判斷節點名稱的小寫是否和參數name的小寫相同
jQuery.nodeName: function( elem, name ) {
  return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
}
//bool類型屬性(attr)還用到boolHook
boolHook = {
  get: function( elem, name ) {
    var
    //使用 .prop來確定這個屬性是否能被理解成布爾類型
    prop = jQuery.prop( elem, name ),
    //獲取
    attr = typeof prop === "boolean" && elem.getAttribute( name ),
    //備注getSetInput = jQuery.support.input;
    //getSetAttribute = jQuery.support.getSetAttribute
    detail = typeof prop === "boolean" ?
    getSetInput && getSetAttribute ?
    attr != null :
 
    // 老IE對缺失的布爾屬性會會構造一個空字符
    // checked/selected需要使用"default-" +
    //備注ruseDefault = /^(?:checked|selected)$/i
    ruseDefault.test( name ) ?
    elem[ jQuery.camelCase( "default-" + name ) ] :
    !!attr :
    //非布爾類型的屬性處理 
    elem.getAttributeNode( name );
 
    return detail && detail.value !== false ?
      name.toLowerCase() :
      undefined;
  },
  set: function( elem, value, name ) {
    if ( value === false ) {
      // 如果設置false則移除布爾屬性
      jQuery.removeAttr( elem, name );
    } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) {
      // IE<8對input的checked/selected 需要特征(property)名稱
      elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name );
 
    // 老IE使用defaultChecked 和defaultSelected
    } else {
      elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true;
    }
 
    return name;
  }
};
 

// IE一些attributes需要特殊處理
if ( !jQuery.support.style ) {
  jQuery.attrHooks.style = {
    get: function( elem ) {
      // 參數為空字符串返回undefined
      // 備注: IE會將css屬性名稱大寫,但是如果我們使用 .toLowerCase(),那么將破壞url中的敏感度導致錯誤,比如background中設置了url
      return elem.style.cssText || undefined;
    },
    set: function( elem, value ) {
      return ( elem.style.cssText = value + "" );
    }
  };
}
View Code

 


jQuery.valHooks 方法
valHooks: {
  option: {
    get: function( elem ) {
           // Blackberry 4.7沒有定義.attributes.value而使用.value
           var val = elem.attributes.value;
           return !val || val.specified ? elem.value : elem.text;
        }
  },
  select: {
        get: function( elem ) {
            var value, option,
               options = elem.options,
               index = elem.selectedIndex,
               one = elem.type === "select-one" || index < 0,
               values = one ? null : [],
               max = one ? index + 1 : options.length,
 
               i = index < 0 ?
               max :
               one ? index : 0;
 
           // Loop through all the selected options
           for ( ; i < max; i++ ) {
               option = options[ i ];
               //IE6-9在重置后不會更新選中狀態
               if ( ( option.selected || i === index ) &&
               //不可用或在不可用option組的option不要返回值
                ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) &&
               ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
                    //獲取置頂的option值
                    value = jQuery( option ).val();
 
                    //單選select直接返回值
                   if ( one ) {
                       return value;
                   }
 
                   //多選Selects循環
                   values.push( value );
                }
           }
 
           return values;
        },
        set: function( elem, value ) {
           var values = jQuery.makeArray( value );
 
           jQuery(elem).find("option").each(function() {
                this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
               });
 
           if ( !values.length ) {
               elem.selectedIndex = -1;
           }
           return values;
        }
    }
},

// Radios and checkboxes getter/setter
if ( !jQuery.support.checkOn ) {
    jQuery.each([ "radio", "checkbox" ], function() {
           jQuery.valHooks[ this ] = {
               get: function( elem ) {
                // Webkit在沒有置頂值的時候會返回 "",我們用on替代
                return elem.getAttribute("value") === null ? "on" : elem.value;
               }
           };
    });
}
jQuery.each([ "radio", "checkbox" ], function() {
    jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
           set: function( elem, value ) {
               if ( jQuery.isArray( value ) ) {
                return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
               }
           }
    });
});
View Code
 
         
  對於val方法的取值部分
if ( elem ) {
    hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; 

    if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
        return ret;
    }
    ret = elem.value;
    return typeof ret === "string" ?
        // handle most common string cases
        ret.replace(rreturn, "") :
        // handle cases where value is null/undef or number
        ret == null ? "" : ret;
}

  通過jQuery.valHooks匹配對應的鈎子處理方法

  

 

  節點屬性的差異對比:

  select : 創建單選或多選菜單

type:"select-one"
tagName: "SELECT"
value: "111"
textContent: "↵ Single↵ Single2↵"

  option : 元素定義下拉列表中的一個選項

tagName: "OPTION"
value: "111"
text: "Single"
textContent: "Single"

  radio : 表單中的單選按鈕

type: "radio"
value: "11111"

  checkbox : 選擇框

type: "checkbox"
value: "11111"

  根據對比select的節點type是'select-one’與其余幾個還不同,所以jQuery在適配的時候采用優先查找type,否則就找nodeName的策略

hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
//如果鈎子匹配到了,並且還存在get方法,那么就要調用這個方法了,如果有返回值就返回當前的這個最終值 if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
    return ret;
} 

 

   這一章比較長了,就到這里,下一章分析CSS的鈎子機制

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM