淺談jQuery構造函數

$()函數到底做的什麼

  jQuery在前端領域路人皆知,對於一向喜歡玩js的博主來說,雖然能力有限,但是還是很喜歡研究他的做爲。那麼一個簡單的美元符號$與一對常見的()括號,jQuery底層到底做了哪些工作,如果你是前端新人,並喜歡刨根問底,你可以看一下下面的介紹。如果你是有經驗的牛人,你可以指出錯誤,畢竟博主還是個半瓶子醋,沒法完全理解。

一、函數調用

$(selector, context) : $()這一個簡單的代碼,其實調用了jQuery的構造函數,其中的selector和context是傳遞給jQuery構造函數的參數表達式,前者代表着選擇器字符串,後者是上下文,jQuery會根據這兩個參數,在頁面中找到相應的dom對象,幷包裝成jQuery對象返回給我們。

var jQuery = function(selector, context) {
    return new jQuery.fn.init(selector, context, rootjQuery);
}


jQuery.fn = jQuery.prototype = {
   init: function (selector, context, rootjQuery) {
      // ...
   }
}

jQuery.fn.init.prototype = jQuery.fn;

調用jQuery()後,返回了一個new jQuery.fn.init(),其實就是jQuery實例,因爲jQuery.fn.init 的原型指向了jQuery.fn,也就是jQuery的原型,這部分不需要解釋了,是原型鏈的問題了。

二、步入函數

就這樣jQuery進入到了init函數中,接下來看下面代碼

// 如果沒有參數,返回當前jQuery對像
if (!selector) {
  return this;
}
//如果selector有nodeType屬性,則是dom對象,返回包裝後的jQuery對象
if (selector.nodeType) {
  this.context = this[0] = selector;
  this.length = 1;
  return this;
}
//如果selector是body,由於body只有一個,並且我們可以明確的指出,所以直接把body元素包裝後返回jQuery對象
if (selector === "body" && !context && document.body) {
  this.context = document;
  this[0] = document.body;
  this.selector = selector;
  this.length = 1;
  return this;
}

由此我們可以知道 $()反回了個空的jQuery對象, $(document.body) 走了第二個if返回了body對象的jQuery對象, $('body')走了第三個if也返回了body對象的jQuery對象。

接下來,jQuery構造函數進入的最難的部分,請看着源碼來讀下面的介紹。

一、如果selector是字符串類型,有兩種情況:

1. 如果是dom元素字符串,如$('<div>')  或者 $('<div>', {title: 'good'})等情況,走一種處理方法

2. 如果是單獨的div選擇器,如$('#box') ,走一種處理方法

3.1. 如果不是上面兩種情況,如果context參數存在並且是jQuery對象,則用這個jQuery對象查找selector,如 $('span', $('body')) 相當於 $('body').find('span')

3.2. 如果不是1、2兩種情況,並沒有context參數,就用根jQuery對象(document的jQuery對象)查找selector,如: $('span') 相當於 $(document).find('span')

4. 其他情況,就是context是dom對象時,用context包裝成jQuery對象查找selector, 如$('span', document.body) => $(document.body).find('span')

二、如果selector是函數,那麼進入ready方法,等待dom加載完畢執行函數,也就是$(function () {...})

三、如果以上都不是,並且selector有selector和context屬性,這代表什麼呢?當然是傳進來的jQuery對象了,如果是這樣,那jQuery返回一個類似的新對象。。。

三、難區詳解(selector是字符串類型的1、2兩種情況)

  由容易到難,現說是id選擇器的情況,再詳細說是dom元素字符串的情況。。。

1. selector是個id選擇器

elem = document.getElementById(match[2]);

//
黑莓手機兼容解決 if (elem && elem.parentNode) { // ie 歐朋 選擇name解決 if (elem.id !== match[2]) { return rootjQuery.find(selector); } this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this;

如果是id選擇器,就直接選出元素包裝成jQuery對象返回。

2. selector是dom元素字符串


這裏又分爲3種情況:

1. 如果selector是一個簡單的dom字符串,如: $('<div>'), $('<div></div>')

  1.1 如果第二個參數context是個純js對象,也就是鍵值對,保存着屬性和屬性值,如$('<div>', {title: 'good'}) ,那麼創建dom對象並賦值這些屬性

  1.2 如果第二個參數存在並且是dom對象,那麼就用這個dom對象創建selector這個dom, 如果context不是dom對象,就用document創建selector這個dom

2. 如果selector並不是一個簡單的字符串,其中有屬性賦值在裏面,如 $('<div id="box">'),進入 jQuery.buildFragment 方法構造dom對象結構

四、dom對象的構造中轉站

這裏不貼代碼了,同學們可以看着源碼走下去。

jQuery.buildFragment中,jquery先取到要轉化成dom的字符串,和要插入到真是dom的dom對象,也就是context參數或者是document對象。

然後根據各種條件判斷,這個dom對象是否是可緩存的,如果是可緩存,查找緩存區是否已經有了,如有有了,直接返回,如果沒有,那麼進入jQuery.clean方法創建並緩存。

五、dom對象的構造工坊

jQuery.clean 函數中, jQuery還是先確定下要創建的dom字符串數組,和在哪個dom下創建。

進入創建循環,看到這樣一句話

if (!rhtml.test(elem)) {
    elem = context.createTextNode(elem);
}

博主看rhtml正則,覺得這個是判斷是不是實體字符和html標籤,如果不是實體字符或html標籤,創建文本節點。

往下看,如果不是實體字符,jQuery把這個字符串轉化爲開閉式的合格dom字符串並且取到了dom的名稱

elem = elem.replace(rxhtmlTag, "<$1></$2>");

var tag = (rtagName.exec(elem) || ["", ""])[1].toLowerCase()

由於tr td option等標籤,是依賴於上級的標籤,所以jQuery在這裏需要考慮如果要創建他們,需要先創建他們上級,所以下面代碼有個

wrap = wrapMap[tag] || wrapMap._default

那我們去查看wrapMap這個對象是什麼

wrapMap = {
  option: [1, "<select multiple='multiple'>", "</select>"],
  legend: [1, "<fieldset>", "</fieldset>"],
  thead: [1, "<table>", "</table>"],
  tr: [2, "<table><tbody>", "</tbody></table>"],
  td: [3, "<table><tbody><tr>", "</tr></tbody></table>"],
  col: [2, "<table><tbody></tbody><colgroup>", "</colgroup></table>"],
  area: [1, "<map>", "</map>"],
  _default: [0, "", ""]
}

我們來到了這裏,看到了這個對象。我們可以看到,這個對象包含了那些需要上級dom才能存活的dom列表,他們的值是一個數組,數組第一個表示的層級關係,比如option是select下的第一級, tr是table下的第二級,數組第二個和第三個元素很明顯分別表示了開口標籤閉口標籤,那麼有什麼用呢?繼續往下看

div.innerHTML = wrap[1] + elem + wrap[2];

看到下面幾行,終於茅塞頓開,原來是這樣創建的依賴上級元素的 dom元素,前後上級標籤中間夾着需要創建dom的字符串,把整個字符串賦值給一個div,構成了完整的dom。知道了後,往回走。

safeChildNodes = safeFragment.childNodes

上面還有一行這個是什麼意思呢?我們去看看safeFragment把

//    h5標籤
nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
    "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",

safeFragment = createSafeFragment(document);

function createSafeFragment(document) {
  var list = nodeNames.split("|"),
  safeFrag = document.createDocumentFragment();

  if (safeFrag.createElement) {
    while (list.length) {
      safeFrag.createElement(
        list.pop()
      );
    }
  }
  return safeFrag;
}

我們找到了這三個重要的表達式,哦,原來這個safeFragment保存着所有h5的標籤

if (context === document) {
  safeFragment.appendChild(div);
} else {
  createSafeFragment(context).appendChild(div);
}

接下來jQuery保存了之前我們創建的div,還不知道要幹什麼,繼續往下看吧。

while (depth--) {
    div = div.lastChild;
}

接下來,jQuery根據層級關係,一層一層的把外部容器去掉,只留下最後一層包裹着我們想要的那個dom。

下面,就是對ie的兩個兼容,ie下自動生成tobody,jQuery做了兼容,ie下破壞首行縮進,jQuery做了兼容。

elem = div.childNodes;
if
(div) { div.parentNode.removeChild(div); if (safeChildNodes.length > 0) { remove = safeChildNodes[safeChildNodes.length - 1]; if (remove && remove.parentNode) { remove.parentNode.removeChild(remove); } } } }

保存我們需要的dom節點後,把此次循環的div刪掉。把插入到safeFragment的div也刪掉,清空乾淨,馬上要進入下一循環

var len;
if (!jQuery.support.appendChecked) {
  if (elem[0] && typeof(len = elem.length) === "number") {
    for (j = 0; j < len; j++) {
      findInputs(elem[j]);
    }
  } else {
    findInputs(elem);
  }
}

上面這一個if針對的 checkbox和radio元素在ie下checked失效的bug

if (elem.nodeType) {
  ret.push(elem);
} else {
  ret = jQuery.merge(ret, elem);
}

終於,保存了此次dom元素到ret數組中。然後下面的一個if是對script標籤的插入處理,博主沒有去看,因爲不常用啊。。

終點

  終於,jQuery方法走完了,走的好慢好艱辛,走過了這次行程,下一次是不是就很好走了O(∩_∩)O~。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章