jQuery2.x源碼解析(DOM操做篇)

jQuery這個類庫最爲核心重要的功能就是DOM操做了。DOM是由w3c制定的爲HTMLXML文檔編寫的應用程序接口,全稱叫作W3C DOM,它使得開發者可以修改html和xml的內容和展示方式,將網頁與腳本或編程語言鏈接起來。javascript

可是標準在各個瀏覽器中的實現是不同的,同時DOM發展也是按部就班的,不斷地增長新的api,所以各個瀏覽器乃至各個版本對於DOM實現的也是不同的。jQuery這個類庫最爲核心的功能,就是可以將各個主流瀏覽器的主流版本的DOM處理方法統一塊兒來,讓開發人員沒必要去過度瞭解各個瀏覽器對於DOM處理的細節與差別,寫一次代碼就能在各個瀏覽器中運行,而且取得相同的效果。css

jQuery這個框架目前的流行程度在降低,除了新的理論新的框架的興起外,一個主要緣由就是各個主流瀏覽器對於DOM處理的差別正在縮小,DOM處理差別大的瀏覽器日益趨於被淘汰,因此jQuery對咱們而言僅是一個封裝了DOM處理的工具,存在的意義也日趨降低。可是從mdn的Element預約義的幾個api就能發現,新增長的api都是在模仿jQuery的,可見jQuery的api是多麼經典。html

阮一峯老師曾經有一篇jQuery-free化的文章,裏面介紹了不少不使用jQuery也能夠高效開發的技巧,這些技巧都是仿照jQuery,並在深刻了解了DOM基礎上封裝起來的。查看jQuery對於DOM操做的源碼,不但會了解到標準的DOM操做方法,學習DOM的api的優化,還能學習到關於瀏覽器兼容的一些「黑魔法」、「黑科技」。java

jQuery封裝的DOM大體能夠分爲3大類:DOM操做、DOM遍歷、DOM事件,篇幅有限,咱們先着重看一看DOM操做相關的jQuery的源碼。下面咱們就一塊兒看看jQuery的DOM操做相關的部分。jquery


提問:jQuery的DOM操做原理是什麼?

答:jQuery的核心思想是簡化DOM的概念,去除出Element外的全部其餘Node接口的概念,咱們只需考慮元素節點Element就能夠了。原始的DOM操做包括Element、Attr、Text、Comment、Document等對象的操做,而在開發中咱們基本上只會處理Element,Element相關的如Attr、Text均可以以字符串的形式操做,因此將DOM的操做簡化爲對Element的操做是可行的。jQuery使用jQuery對象的形式將Element對象進行封裝,把複雜的DOM定義簡化爲對jQuery對象,使即便不熟悉DOM概念的人也能夠輕鬆操做DOM對象。web

將Element對象封裝爲jQuery對象,再在jQuery對象的原型上面定義出統一接口,作成一個門面模式。用戶不用去考慮不一樣瀏覽器下Element處理的差別,由於內部用策略模式或者判斷樹將各個主流瀏覽器的差別性已經屏蔽掉了;而且作好了持續升級的準備,以對應瀏即便覽器不斷更新換代,帶來新的功能,淘汰舊的功能後,基於jQuery開發的功能依舊能夠不受影響地正常運行。ajax

jQuery對於DOM封裝的另外一個核心就是其緩存功能,jQuery將jQuery對象的變量(如事件、動畫、第三方擴展的屬性)都保存緩存中,而緩存又保存在實際Element對象中,因此當這個Element對象被再一次封裝爲jQuery對象的時候,是能夠繼承以前的緩存進而能夠繼續以前的操做。這個特性使得咱們不用特地去緩存jQuery對象的引用,須要用的時候隨時能夠再一次將Element對象封裝爲jQuery對象,而不用擔憂兩個的jQuery對象操做上會產生數據上的差別。即jQuery對象不保存任何Element元素相關信息,全部的相關信息都保存在DOM自己中。咱們開發jQuery插件的時候,也要保證這一原則,不然插件在使用過程當中可能會出現問題。算法

此外,jQuery優化了DOM處理過程,讓DOM處理效率更高,由於它封裝了一些DOM處理的技巧,固然若是你已經熟知了這些技巧,那麼你使用jQuery後效率不會有提高反而會降低(jQuery在統一瀏覽器操做也會犧牲一部分性能,不少受兼容性限制的底層高效的api會被用更耗時但兼容性更好的方法取代掉)。由於jQuery的優化是技巧上面的,因此確定不及Virtual DOM這種從渲染算法上優化明顯。編程

最重要的是jQuery對象支持集合操做、鏈式操做,這極大的簡化了DOM處理,筆者以前已經分析過jQuery的api的設計原理,這些原理一樣適用於處理DOM,使得DOM操做變得簡便、能夠集合處理、能夠鏈式處理、能夠異步處理、易於擴展......api


提問:jQuery的DOM操做API由哪些部分組成?

答:DOM操做的API也不少,大體能夠分爲5大類:

1.DOM元素的建立:

jQuery(html,[ownerDoc])
jQuery.fn.clone([Even[,deepEven]])
jQuery.parseHTML(html,[ownerDoc])
jQuery.fn.html([val|fn])

2.DOM元素的插入:

jQuery.fn.append(content|fn)
jQuery.fn.appendTo(content)
jQuery.fn.prepend(content|fn)
jQuery.fn.prependTo(content)
jQuery.fn.replaceWith(content|fn)
jQuery.fn.after(content|fn)
jQuery.fn.before(content|fn)
jQuery.fn.insertAfter(content)
jQuery.fn.insertBefore(content)
jQuery.fn.replaceAll(selector)
jQuery.fn.wrap(html|ele|fn)
jQuery.fn.unwrap()
jQuery.fn.wrapAll(html|ele)
jQuery.fn.wrapInner(html|ele|fn)

3.DOM元素的修改:

jQuery.fn.attr(name|pro|key,val|fn)
jQuery.fn.removeAttr(name)
jQuery.fn.prop(n|p|k,v|f)
jQuery.fn.removeProp(name)
jQuery.fn.html([val|fn])
jQuery.fn.text([val|fn])
jQuery.fn.val([val|fn|arr])

4.DOM元素的刪除:

jQuery.fn.empty()
jQuery.fn.remove([expr])
jQuery.fn.detach([expr])
jQuery.fn.html([val|fn])
jQuery.fn.replaceAll(selector)
jQuery.fn.wrap(html|ele|fn)
jQuery.fn.unwrap()
jQuery.fn.wrapAll(html|ele)
jQuery.fn.wrapInner(html|ele|fn)

5.DOM的ready:

jQuery(callback)
jQuery.holdReady(hold)
jQuery.ready(hold)

能夠看出,DOM操做的API是很是多的。可是不少API都是對底層API的封裝,真正核心的API沒有幾個,咱們能夠概括爲這麼幾個(包括私有API):

1.DOM元素的建立:

jQuery.fn.clone([Even[,deepEven]])
buildFragment

2.DOM元素的插入

domManip
buildFragment

3.DOM元素的修改:

access

4.DOM元素的刪除:

jQuery.fn.cleanData( elems )
remove( elem, selector, keepData )

5.DOM的ready

jQuery.ready

接着咱們將分別重點看看這幾個函數。


提問:jQuery建立元素是如何實現的?

筆者在設計篇的博客中總結了jQuery函數是數種最經常使用的api的重載函數,其中有一種就是經過Html字符串生成元素對象:

  jQuery(html)

  jQuery(html,ownerDocument)

  jQuery(html,attributes)

這些函數使用起來很是方便,那麼jQuery是如何實現這種功能的呢?

答:咱們建立元素的時候,可使用document.createElement來建立Element,可是更高效的一個方法是用innerHTML將html文檔文本轉變爲其對應的元素對象。jQuery函數是個對外暴露的混合接口,其中經過Html文檔文本生成元素對象工做就由jQuery.parseHTML來執行的,可是他還不是最底層的實現方法,真正的底層方法是buildFragment。

buildFragment主要做用是建立一個DocumentFragment對象,將不一樣種類的參數,統一地封裝爲DocumentFragment子元素的形式。之因此要封裝爲DocumentFragment是爲了插入的時候效率更高效,由於buildFragment其實是爲元素節點的插入而準備的。咱們看一看buildFragment的流程圖:

注意這麼幾個地方:

1.該函數支持多參數,支持Node對象、jQuery對象、普通文本、Html文檔文本等4種參數類型

2.對於Html文檔文本,他會調用innerHTML來生成對應的Element對象

3.調用innerHTML經過Html文檔文本生成DOM對象時,jQuery會對於td、tr、option等只能在特定父元素上面建立的元素,將指定的父元素套在Html文檔文本外面,確保這些元素是能夠建立的,並在innerHTML執行以後,移除多生成的父元素。

4.最後全部元素會被統一套在一個DocumentFragment中,並返回。

5.對於封裝到DocumentFragment裏面的script標籤,jQuery會判斷它是否是已經append到document上面了,若是是表示已經執行過了,jQuery會調用內部緩存接口dataPriv爲其打個標記,表示已經執行過了,從此再插入到document的時候不會再執行這些script標籤了。

以上就是buildFragment的所有邏輯,buildFragment生成的是DocumentFragment對象,而jQuery.parseHTML卻不是,在jQuery.parseHTML中,jQuery又把生成的元素從DocumentFragment對象中取出來,筆者認爲這樣作其實是對性能的浪費,可是經過調用buildFragment函數卻實現了整個經過html字符串建立元素對象這個複雜邏輯的複用,性能的犧牲是能夠接受的。


提問:jQuery是如何克隆元素的?

答:Element.prototype.cloneNode是克隆元素的DOMAPI,jQuery底層天然也用的它。可是事件、緩存、動畫、子元素的內容克隆起來是沒法保證確定能被克隆的,因此jQuery將這些東西都經過dataPriv緩存起來,這樣克隆的時候可讓新克隆好的對象也引用dataPriv緩存的事件等系統對象,就完成了對事件、緩存、動畫等內容的克隆。須要注意的是這個過程是淺拷貝。

同時對於script元素的克隆,也和buildFragment同樣,判斷它是否是已經append到document上面了,一樣會給已經執行過的script元素打個標記。這樣這些被克隆的新的script標籤,在插入到document的時候,會和原標籤有相同的表現行爲。

同時在克隆的時候,須要注意早期的webkit瀏覽器中是不會克隆表單元素的defaultValue、checked和當前用戶輸入的value的,也就是說用戶輸入的值和表單元素的默認值是不會被克隆的。jQuery會先實驗性的調用cloneNode克隆一個input元素對象,經過新克隆的input元素是否存在defaultValue和checked,來判斷當前瀏覽器是否會存在不能克隆defaultValue和checked的問題,若是有這樣的問題就調用原對象的defaultValue和checked覆蓋掉新克隆對象的defaultValue和checked。


提問:jQuery是如何實現元素的插入的?

答:jQuery在插入的時候,都會調用domManip作些準備工做。jQuery有多種插入api,可是不管哪一個api都調用了這個domManip函數。咱們看看domManip函數的具體流程:

須要注意的地方有:

1.經過調用buildFragment將全部待插入的參數統一封裝爲一個DocumentFragment對象,由於buildFragment函數不支持經過函數做爲參數來建立元素對象,所以若是入參是個函數,要先調用一次,生成元素對象後,再遞歸調用domManip從新走一遍插入的邏輯。

2.jQuery修改了待插入元素中script元素的type屬性,使其被插入到document時候不會執行裏面的JavaScript代碼。

3.回調真正的插入函數的時候,若是插入目標有多個的話,就將被插入對象克隆,確保每一個插入目標對象都能得到待插入對象或者是其克隆對象。

4.最後階段,統一處理script元素,將須要執行的script經過調用globalEval函數執行。


提問:domManip函數在部分瀏覽器中,對帶有checked的Html字符串作了什麼特殊處理?

domManip調用buildFragment的時候,除了對函數參數作了特殊處理外,還對帶有checked的Html字符串作了特殊的處理,這是爲何?

複製代碼
// We can't cloneNode fragments that contain checked, in WebKit
if ( isFunction ||
    ( l > 1 && typeof value === "string" &&
    true && rchecked.test( value ) ) ) {
    return collection.each( function( index ) {
        var self = collection.eq( index );
        if ( isFunction ) {
            args[ 0 ] = value.call( this, index, self.html() );
        }
        domManip( self, args, callback, ignored );
    } );
}
複製代碼

答:這是筆者一直疑惑的地方,從jQuery的註釋來看,是在一些瀏覽器上,沒法正確地克隆元素的checked。可是咱們以前分析過,jQuery的clone函數裏面已經作了對這種狀況的兼容,此處domManip再作處理是徹底沒有必要。同時這麼作還會出現一個bug,就是若是插入目標有多個的話,沒法知足確保每一個插入目標對象都能插入到待插入對象這一功能。如:

var div1 = $("<div id='div1'>");
var div2 = $("<div id='div2'>");
var span = $("<span>");

div1.add(div2).append("<span checked>",span)

理論上,上述代碼中的div1和div2裏面都會有兩個span,可是實際上在部分瀏覽器(不支持checked克隆的瀏覽器)中div1中只有一個span。這是由於當span插入div2的時候,jQuery沒有克隆這個span,而是直接插入進div2中,這一span會自動從div1中移除。這一就形成了不一樣瀏覽器解析效果的不一樣,應該算jQuery的一個bug。


提問:jQuery在插入元素的時候,對script元素一共作了哪些處理?

答:從上面的流程中咱們知道,jQuery在插入的時候,把很大的經歷都花在對script元素的處理上。整體思路爲,爲已經執行過的script元素加標記,再修改其的type屬性,使其插入後不會執行,而後等插入後再將type屬性修改回來,最後判斷未標記的script標籤是否已經插入到document上面,若是是表示應該運行,jQuery會經過globalEval執行這些script。咱們看看globalEval的流程:

從圖中能夠看出,globalEval的執行邏輯不夠簡化。運行腳本無外乎兩種方式:1.動態建立script標籤 2.使用eval。 globalEval將這兩種方式都使用了,筆者認爲jQuery只需封裝一種便可。

上述流程中,有幾點須要注意一下:

1.對於有src屬性的script元素,jQuery是用過$.ajax模塊執行的。這樣會受到$.ajaxSettings裏面配置的影響。

2.對於有src屬性的script元素,會根據跨域和不跨域使用兩種不一樣的運行方式,這個咱們之後能夠在jQuery.ajax的解析中再分析。

3.eval在嚴格模式下和非嚴格模式下使用方法不一樣:

eval執行的時候,可使用不一樣的做用域。

你能夠間接的使用 eval(),若是這麼作視爲javascript代碼在當前做用域上執行。你也能夠用變量來引用eval,而後調用它,若是你這麼作了,那麼這個時候目標字符串中的javascript代碼將被直接視爲在全局做用域下執行, 這是由於 ES 規範裏明確規定了對 eval 的直接調用和間接調用會被區別對待。如:

function test() {
  var x = 2, y = 4;
  console.log(eval("x + y"));  // 結果是6
  var geval = eval;
  console.log(geval("x + y")); // 報x未定義的錯
} 

可是在嚴格模式下,沒有這樣調用的效果,因此jQuery使用了建立script標籤的形式來實現 javascript 代碼在全局做用域運行的效果。


提問:元素的移除是怎麼實現的?

答:在刪除一個元素的時候,jQuery要將jQuery對象對一個Element的全部擴展移除掉,對於jQuery1.x這個過程是必須的,不然會出現內存泄漏;而在jQuery2.x中,這個過程依舊保留。

由於jQuery的全部擴展信息都緩存在Element對象自己裏面,因此想要將一個jQuery封裝過的Element對象還原只要將這些信息刪除便可。此外jQuery對用戶事件使用了addEventListener(IE是attachEvent)註冊到元素對象上,因此刪除完緩存信息還要調用removeEventListener解除jQuery爲元素對象增長的事件監聽。這一過程筆者會專門在事件篇分析,這裏再也不深究。達到以上兩點,就能夠將去除jQuery對Element對象的全部擴展。

jQuery.cleanData就是實現這個功能的,而jQuery在全部涉及到DOM移除的操做的時候,都會調用這個函數。


提問:jQuery.fn.html是如何實現的?

答:咱們分析了Element的建立、插入、移除,而jQuery.fn.html這個API同時包括了這三個功能。jQuery.fn.html自己是對Element.prototype.innerHTML的封裝,對於元素的建立,jQuery也是使用這個方法,因此jQuery.fn.html並無調用buildFragment,而是直接調用innerHTML。不過對於tr、td、option等只能在特定父元素下建立的元素,jQuery不會直接用innerHTML,而是會經過append來實現,這樣就會調用buildFragment生成這種對象。如:

$("<div>").html("<tr>")
$("<div>")[0].innerHTML = "<tr></tr>"

下面的方法是沒法建立出tr對象的,而上面的方法是能夠的。由於jQuery會調用buildFragment來建立,會補充table和tbody兩個父元素套在「tr」html文本的外邊,等建立成功後再移除掉。

jQuery.fn.html這樣作,使得其能夠在不一樣狀況下優先使用在保證功能實現的狀況下效率最高的方法。

同時jQuery.fn.html仍是個setter和getter的重載函數,咱們在設計篇分析過,jQuery使用模板模式加函數柯里化實現setter和getter重載的方法access,這個jQuery.fn.html就是access的一個應用。

jQuery.fn.html是一個很是典型的「jQuery的DOMapi」的實現,支持setter和getter、對瀏覽器兼容作了調整、對應innerHTML卻未必直接調用他,還有不少api使用了這樣的封裝形式,這裏再也不一一分析。


提問:jQuery是如何作DOM的ready的?

答:咱們說的DOM的ready是指的是DOMContentLoaded這個事件,可是這個事件在早期的ie瀏覽器中並未提供。因此人們開發的最佳實踐是把script標籤放到文檔的最下端,這樣一方面沒必要在乎DOM的ready的實現,由於文檔解析到最後,須要解析的Element對象都解析完了,並放入document裏面了,在最下邊運行的JavaScript代碼雖然仍是在DOM的ready以前運行,可是是不會出現因被操作的Element還沒渲染而出現問題;另外一方面,放到最後,能夠去除script標籤對css、圖片的資源的加載影響,由於script是同步加載解析,而圖片和css都是異步的。可是jQuery沒法左右人們的script是放到head裏面仍是最後面,因此jQuery沒法採起這個方案,仍是要回到DOMContentLoaded事件上。

在jQuery1.x系列中,由於要兼容不支持DOMContentLoaded事件的瀏覽器(ie六、ie七、ie8),因此使用了一個hack來在IE下模擬DOMContentLoaded事件,就是調用setTimeout定時查看瀏覽器的Scroll是否渲染出來,若是Scroll完成,能夠近似表示爲DOM的ready也完成了。在jQuery2.x系列中,由於不須要兼容這種的瀏覽器,因此直接調用DOMContentLoaded就能夠實現DOM的ready。

可是DOMContentLoaded和onLoad同樣,瀏覽器只執行一次,jQuery用什麼判斷是否已經執行過呢?document.readyState就是判斷這個的依據。readyState是document的屬性,總共有3個值:
loading:    文檔正在加載中
interactive: 文檔已經加載完成,正在進行css和圖片等資源的加載 
complete:     文檔的因此資源加載完成
咱們所說的DOM的ready就是指的document.readyState等於interactive的時候。document.readyState這個api在ie8就已經有了,可是他不兼容interactive值,而在ie9之後補充了interactive狀態,使得咱們可使用。
遺憾的是根據document.readyState 在IE9上仍是存在bug,所以在ie上面繼續使用jQuery1.x的那種和Scroll渲染配合完成判斷DOM的ready的方案。

判斷完以後如何回調呢?很簡單,就是用Promise,jQuery經過new一個$.Deferred(promise)對象來實現對DOM的ready的回調,在DOMContentLoaded中將這個promise給resolve掉,這樣就執行了以前註冊的回調函數,同時後面新註冊的回調也會馬上執行。可是在調用promise以前,jQuery執行了一次setTimeout,在回調篇咱們分析出jQuery.Promise是不會產生異步,這和標準的promise規範是不同的,全部jQuery本身又手動作了一次setTimeout來實現異步。這樣使得不管使用在DOM的ready以前註冊的回調仍是以後註冊的回調都會在異步中執行。


提問:如何自定義jQuery的ready事件?

jQuery的ready是瀏覽器的DOMContentLoaded事件,是瀏覽器解析完整個文檔的事件,即DOM已經被解析完,頁面已經初始化完成。可是實際開發中會有DOM初始化已經完成,可是頁面卻未初始化的完成狀況。好比從事cordova開發,有一個deviceready的概念,在這個事件執行前,頁面視爲未初始化完畢。或者一些操做是要在圖片和css加載完才能運行的,而這些要使用onload事件。如何將deviceready或者onload代替DOMContentLoaded呢?

答:jQuery是提供了這樣的api,就是jQuery.holdReady,他能夠暫停與恢復jQuery。當deviceready前執行一次jQuery.holdReady(true),在onload或者deviceready完成後再執行一次jQuery.holdReady(),就可讓jQuery的ready移動到onload或者deviceready以後,完成頁面的初始化工做。如:

複製代碼
$.holdReady(true);
document.addEventListener("load",()=>{
    $.holdReady();
})
$(()=>{
    //對圖片的操做
    $("image");
})
複製代碼

暫停和恢復都是能夠任意次數地疊加的,即若是執行x次$.holdReady(true),必須再執行x次$.holdReady()以後,jQuery的ready纔會執行,不過$.holdReady(true)必須放到jQuery的ready前執行纔有效。

他是如何實現的呢?很是簡單,就是定義一個等待棧變量——readyWait,每次執行$.holdReady(true)都會增長壓棧,而每次$.holdReady()執行都會彈棧,等空棧的時候就執行jQuery.ready函數,即將promise給resolve掉。


 

總結

以上就是jQuery的DOM操做的源碼解析,從這一節開始代碼的難度逐漸提高,從jQuery的issues來看,多數都是集中在這個部分,並且jQuery也爲這些issues打了大量的補丁,「#xxxx」的註釋也比以前的代碼多了不少。從筆者對於這些代碼分析的結果來看,不少地方仍是有優化的空間,好比globalEval這種複雜的執行方式。jQuery這個庫雖然比較簡單,可是歷史不短,代碼貢獻者有200多人,裏面不免有由於歷史緣由而留下的可優化空間,整體而言jQuery很是優秀,有興趣的話歡迎你們一塊兒閱讀。

相關文章
相關標籤/搜索