jQuery的DOM操作模塊封裝了DOM模型的insertBefore()、appendChild()、removeChild()、cloneNode()、replaceChild()等原生方法。分為5個子模塊來實現:插入元素、刪除元素、複製元素、替換元素和包裹元素,本節講解第一個子模塊:插入元素 ...
jQuery的DOM操作模塊封裝了DOM模型的insertBefore()、appendChild()、removeChild()、cloneNode()、replaceChild()等原生方法。分為5個子模塊來實現:插入元素、刪除元素、複製元素、替換元素和包裹元素,本節講解第一個子模塊:插入元素
插入元素模塊可用於新增DOM節點,修改文本節點等,API如下:
- append(content) ;在被選元素子節點的末尾插入指定內容,內部調用appendChild(elem)方法 ;content可以是HTML代碼、函數(返回html代碼)、DOM元素 或 jQuery對象,下同
- prepend(content) ;在被選元素子節點的頭部插入指定內容
- before(content) ;在匹配元素集合的每個元素之前插入content。
- after(content) ;在匹配元素集合的每個元素之後插入內容
- appendTo(target) ;將匹配元素中的每個元素插入目標元素末尾 ;內部調用append()方法 ;這四個方法執行後都會調用pushStack()方法,匹配的是全部插入的DOM對象引用。
- prependTo(target) ;將匹配元素中的每個元素插入目標元素開頭 ;內部調用prepeng()方法
- insertBefore(target) ;將匹配元素中的每個元素插入目標元素之前 ;內部調用before()方法
- insertAfter(target) ;將匹配元素中的每個元素插入目標元素之後 ;內部調用after()方法
上面的前四個後面四個是一一對應的,例如:
writer by:大沙漠 QQ:22969969
$('#d').append('<p>1</p>') $('<p>1</p>').appendTo($('#d')); //這兩條代碼的作用是相同的
其它幾個API也是同樣的作用:prepend和prependTo、before和insertBefore、after和insertAfter都是一樣的。
舉個慄子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script> </head> <body> <div id="d"> <p id="p">Hello,World!</p> </div> <h1>insertBefore測試</h1> <h2>insertAfter測試</h2> <button id="b1">append測試</button> <button id="b2">prepend測試</button> <button id="b3">before測試</button> <button id="b4">insertAfter測試</button> <script> $('#b1').click(()=>{ $('#d').append('<p>append測試</p>') //在div元素的子節點末尾添加一個<p>1</p>節點,這裡的參數是一個html代碼 }) $('#b2').click(()=>{ $('#d').prepend('<p>prepend測試</p>') //在div元素的子節點頭部添加一個<p>2</p>節點,這裡的參數是一個html代碼 }) $('#b3').click(()=>{ $('h1').insertBefore('#d') //在div元素元素之前添加$('h1')元素,這裡的參數是一個jQuery對象 }) $('#b4').click(()=>{ $('h2').insertAfter($('#d')) //在div元素元素之後添加$('h2')元素,這裡的參數也是一個jQuery對象 }) </script> </body> </html>
渲染如下:
對應的DOM樹如下:
我們定義了四個按鈕,分別在div的子節點之前、子節點預設,div之前位置和div之後位置插入一個DOM元素,每個按鈕點擊一次,執行後頁面如下:
對應的DOM樹如下:
源碼分析
append、prepend、before和after的實現如下:
jQuery.fn.extend({ append: function() { //在被選元素子節點的末尾插入指定內容,內部調用appendChild(elem)方法 return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 ) { this.appendChild( elem ); } }); }, prepend: function() { //在被選元素子節點的頭部插入指定內容,內部調用insertBefore( elem, this.firstChild )方法 return this.domManip(arguments, true, function( elem ) { if ( this.nodeType === 1 ) { this.insertBefore( elem, this.firstChild ); } }); }, before: function() { //在匹配元素集合的每個元素之前插入內容 if ( this[0] && this[0].parentNode ) { //如果該元素有父元素 return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this ); }); } else if ( arguments.length ) { var set = jQuery.clean( arguments ); set.push.apply( set, this.toArray() ); return this.pushStack( set, "before", arguments ); } }, after: function() { //在匹配元素集合的每個元素之後插入內容 if ( this[0] && this[0].parentNode ) { //如果該元素有父元素 return this.domManip(arguments, false, function( elem ) { this.parentNode.insertBefore( elem, this.nextSibling ); }); } else if ( arguments.length ) { var set = this.pushStack( this, "after", arguments ); set.push.apply( set, jQuery.clean(arguments) ); return set; } }, /*略*/ })
可以看到內部都調用了domManip這個函數,執行時第一個傳入arguments,也就是我們調用append、prepend等傳入的參數,domManip會把參數轉化為一個DOM對象,然後調用參數3,參數3負責執行最後的DOM操作,domManip實現如下:
jQuery.fn.extend({ domManip: function( args, table, callback ) { //jQuery插入元素實現的核心,負責轉換html代碼為DOM元素,然後調用傳入的回調函數插入DOM元素 var results, first, fragment, parent, value = args[0], //例如$().append()的第一個參數,比如:,<p>123</p> scripts = []; //存儲args[0]里的script腳本 // We can't cloneNode fragments that contain checked, in WebKit if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) { return this.each(function() { jQuery(this).domManip( args, table, callback, true ); }); } if ( jQuery.isFunction(value) ) { //如果args[0]是函數,則遍歷匹配元素集合,執行args[0]函數,並把返回值作為參數,再次遍歷執行domManip()函數 return this.each(function(i) { var self = jQuery(this); args[0] = value.call(this, i, table ? self.html() : undefined); //在每個元素上執行該函數,返回一個DOM元素 self.domManip( args, table, callback ); //迭代調用.domMapnip()方法 }); } if ( this[0] ) { //如果至少有一個匹配元素 parent = value && value.parentNode; // If we're in a fragment, just use that instead of building a new one if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) { results = { fragment: parent }; //在1.7.1中jQuery.support.parentNode方法未定義,所以這段代碼不會執行,在1.8中已經移除這段代碼。 } else { results = jQuery.buildFragment( args, this, scripts ); //調用buildFragment()把html代碼轉換為DOM元素。返回一個對象。對象里的fragment屬性包含了轉換後的DOM元素,cacheable表示緩存狀態,指示了該HTML元素是否可以緩存 } fragment = results.fragment; //fragment是轉換後的DOM元素 if ( fragment.childNodes.length === 1 ) { //如果文檔片段只有一個子元素 first = fragment = fragment.firstChild; //重置變數fragment為子元素,因為插入單個子元素比插入含有單個子元素的文檔片段會稍快些。 } else { first = fragment.firstChild; //否則把first作為第一個子節點。 } if ( first ) { //如果子節點存在 table = table && jQuery.nodeName( first, "tr" ); //如果table參數為true,且first是tr節點,那麼設置table為true,否則為false。 for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) { //遍歷當前匹配的元素 callback.call( //執行callback函數,上下文是每個匹配元素,即callback函數內的this指針。參數是轉換後的DOM元素,即傳給callback的參數,也就是append、prepend等試下內部執行doMainip()傳遞的參數3這個函數 table ? root(this[i], first) : //修正目標對象,如果待插入元素是tr元素,則調用函數root()檢查當前元素(容器元素)是否是table元素,如果是則返回tr元素的父元素tbody作為目標元素。 this[i], //否則把匹配元素作為call的第一個參數,即this指針。 // Make sure that we do not leak memory by inadvertently discarding // the original fragment (which might have attached data) instead of // using it; in addition, use the original fragment object for the last // item instead of first because it can end up being emptied incorrectly // in certain situations (Bug #8070). // Fragments from the fragment cache must always be cloned and never used // in place. results.cacheable || ( l > 1 && i < lastIndex ) ? //修正待插入元素,如果返回的文檔片段是可緩存的或者不能緩存但是當前匹配元素大於1個且當前操作不是操作最後一個匹配元素時,總是插入該文檔片段的副本。 jQuery.clone( fragment, true, true ) : //則總是插入該文檔片段的副本,調用jQuery.clone深度複製事件和數據 fragment //否則插入文檔本身 ); } } if ( scripts.length ) { //執行轉換後的DOM目標元素的script元素 jQuery.each( scripts, evalScript ); //jQuery.buildFragment()和jQuery.clean()利用瀏覽器的innerHTML機制把HTML代碼轉換為DOM元素,但是轉換後的script元素並不會自動載入和執行,需要手動處理。 } } return this; //返回匹配元素集合,以支持鏈式操作。 } /*略*/ })
buildFragment是用於將html代碼轉換為對應的DOM節點的,它首先嘗試從緩存中獲取對應的DOM對象,如果沒有緩存則通過document.createDocumentFragment創建一個文檔碎片,然後則再調用$.clean將html代碼轉換為一個DOM對象(通過設置innerHTML來實現的),代碼比較多,不再貼了。
對於appendTo、prependTo、insertBefore和insertAfter來說,它們在會在內部定義一個臨時的jQuery對象,然後再調用對應的append、prepend、before和after來實現的,如下:
jQuery.each({ appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { //給jQuery添加appendTo、prependTo、insertBefore、insertAfter、replaceAll方法 name是要添加的方法名,如appendTo,original標識對應的已有方法名,如append var ret = [], insert = jQuery( selector ), //構造一個jQuery對象,指向目標元素集合,這就是內部的臨時的jQuery對象 parent = this.length === 1 && this[0].parentNode; //如果操作的html只有一個頂級節點,則把parent設置為父元素引用,否則設為false ;比如:$('<p>222</p>').appendTo('#dd'); if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) { //如果待插入元素只有一個,並且是在文檔片段中,同時目標元素也只有一個, insert[ original ]( this[0] ); //則直接將待插入元素插入目標元素,不需要遍歷目標元素集合。這樣速度稍快一點。 return this; } else { for ( var i = 0, l = insert.length; i < l; i++ ) { //遍歷目標元素集合,迴圈體內insert[i]是每一個目標元素 var elems = ( i > 0 ? this.clone(true) : this ).get(); //elems是要插入的元素集合,第一次插入的是待插入元素集合,之後插入的則是它的副本。調用對應的已經有方法名插入elems元素 jQuery( insert[i] )[ original ]( elems ); //調用目標元素的original方法,參數是elems ret = ret.concat( elems ); } return this.pushStack( ret, name, insert.selector ); //用包含了待插入元素集合和它的副本的數組ret構造一個新jQuery對象並返回。 } }; });
通過代碼可以看到appendTo、prependTo、insertBefore和insertAfter只是對append、prepend、before和after的一個封裝而已。