歡迎來個人專欄查看系列文章。javascript
可能你會以爲這個名字很奇怪這個名字很奇怪,爲何叫作 domManip,即所謂的 dom 微操做。html
其實在 jQuery 中有不少重要的 dom 操做,這些操做使用的頻率都很是高,不過這些操做廣泛有一個特色,就是須要進行微調,好比將字符串轉換成 elem 元素,判斷是否爲 script 腳本。java
因此 jQuery 內部一個統一的作法,就是採用 callbacks 的方式,先對要進行 dom 操做的內部函數執行 domManip 操做,而後回調執行任務。node
jQuery 內有幾個方法調用了 domManip 函數,他們分別以下:git
jQuery.fn.extend( { // 在最後一個子元素後添加 append: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.appendChild( elem ); // 原生方法 } } ); }, // 在第一個子元素前添加 prepend: function() { return domManip( this, arguments, function( elem ) { if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { var target = manipulationTarget( this, elem ); target.insertBefore( elem, target.firstChild ); // 原生方法 } } ); }, // 在當前節點前添加 before: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this ); // 原生方法 } } ); }, // 在當前節點後添加 after: function() { return domManip( this, arguments, function( elem ) { if ( this.parentNode ) { this.parentNode.insertBefore( elem, this.nextSibling ); // 原生方法 } } ); }, replaceWith: function() { var ignored = []; return domManip( this, arguments, function( elem ) { var parent = this.parentNode; if ( jQuery.inArray( this, ignored ) < 0 ) { jQuery.cleanData( getAll( this ) ); if ( parent ) { parent.replaceChild( elem, this ); } } }, ignored ); } } );
仔細觀察一下,這幾個函數都有一個特色,就是有關於 domManip 的參數 domManip(this, arguments, callback)
,而後在 callback 函數裏面經過原生 js 來實現:github
// 一個簡單的 jQuery.fn.extend( { append: function(elem){ this[0].appendChild(elem); return this; }, prepend: function(elem){ this[0].insertBefore(elem, this[0].firstChild); return this; }, before: function(elem){ if(this[0].parentNode){ this[0].parentNode.insertBefore(elem, this[0]); } return this; }, after: function(elem){ if(this[0].parentNode){ this[0].parentNode.insertBefore(elem, this[0],nextSibling); } return this; } } );
我以前就跟同窗討論過一個問題,就是如何用原生的方法將字符串轉換成 dom 對象,在 jQuery 裏面直接jQuery.parseHTML()
,原生的話,能夠用下面的:segmentfault
function parseHtml(str){ var div = document.createElement('div'); if(typeof str == 'string'){ div.innerHTML = str; } return div.children[0]; }
雖然非常摳腳,但也是一種方法。數組
其實在 jQuery 內部,或者說 jQuery.parseHTML
方法以內,使用的是另一個方法來創建 str 到 elem 的轉換,那就是 buildFragment 方法。這個方法用於創建文檔碎片,你不要糾結這個方法在 jQuery 中出現幾回,我明確的告訴你,它只在兩個地方出現,分別是 domManip 函數裏和 parseHTML 函數裏。app
在以前,有必要先了解一下 createDocumentFragment,文中有幾句話說的很好:DocumentFragments are DOM Nodes. They are never part of the main DOM tree. The usual use case is to create the document fragment, append elements to the document fragment and then append the document fragment to the DOM tree. 。它雖然也一樣佔內存,卻比 createElement
方法好多了。dom
因此,當之後再碰到 create 無需渲染的 dom 的時候,要使用 document.createDocumentFragment
替代 document.createElement
。
function buildFragment( elems, context, scripts, selection, ignored ) { var elem, tmp, tag, wrap, contains, j, // context 通常爲 document fragment = context.createDocumentFragment(), nodes = [], i = 0, l = elems.length; for ( ; i < l; i++ ) { elem = elems[ i ]; if ( elem || elem === 0 ) { // Add nodes directly if ( jQuery.type( elem ) === "object" ) { // Support: Android <=4.0 only, PhantomJS 1 only // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); // 非 HTML 標籤 } else if ( !rhtml.test( elem ) ) { nodes.push( context.createTextNode( elem ) ); // 將 str 轉換成 html dom } else { tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); // 得到 標籤 類型,處理特殊狀況 tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); wrap = wrapMap[ tag ] || wrapMap._default; tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; // 通常從 0 開始 j = wrap[ 0 ]; while ( j-- ) { tmp = tmp.lastChild; } // 在這裏合併到 nodes 裏面 jQuery.merge( nodes, tmp.childNodes ); // 返回 div tmp = fragment.firstChild; // 清空 tmp.textContent = ""; } } } // 清空 fragment fragment.textContent = ""; i = 0; while ( ( elem = nodes[ i++ ] ) ) { // 跳過已經存在的 context if ( selection && jQuery.inArray( elem, selection ) > -1 ) { if ( ignored ) { ignored.push( elem ); } continue; } contains = jQuery.contains( elem.ownerDocument, elem ); // 添加到 fragment 內部,按照順序,並得到 scripts tmp = getAll( fragment.appendChild( elem ), "script" ); // Preserve script evaluation history if ( contains ) { setGlobalEval( tmp ); } // Capture executables if ( scripts ) { j = 0; while ( ( elem = tmp[ j++ ] ) ) { if ( rscriptType.test( elem.type || "" ) ) { scripts.push( elem ); } } } } return fragment; }
最後的返回結果是 fragment,但它並非咱們想要的 dom,而真正的結果應該是:fragment.childNodes
,一個 dom 僞數組。
其實本文的重點應該是 domManip 方法,不急,如今開始來說。
前面已經介紹了五個基本的 domManip 用法,下面是幾個擴展,也就是反過來用,也算是間接使用 domManip 吧:
jQuery.each( { appendTo: "append", prependTo: "prepend", insertBefore: "before", insertAfter: "after", replaceAll: "replaceWith" }, function( name, original ) { jQuery.fn[ name ] = function( selector ) { var elems, ret = [], // 新建一個 jQuery 對象 insert = jQuery( selector ), last = insert.length - 1, i = 0; for ( ; i <= last; i++ ) { elems = i === last ? this : this.clone( true ); jQuery( insert[ i ] )[ original ]( elems ); // 將 elems 存入 ret push.apply( ret, elems.get() ); } // 返回一個新的 jQuery 對象 return this.pushStack( ret ); }; } );
這又是五個方法,不過是和以前那五個方法恰好先反的邏輯,實用。
來看看 domManip 函數:
function domManip( collection, args, callback, ignored ) { // var concat = [].concat; 用於將僞 args 轉換成真是的數組 args = concat.apply( [], args ); var fragment, first, scripts, hasScripts, node, doc, i = 0, l = collection.length, iNoClone = l - 1, value = args[ 0 ], isFunction = jQuery.isFunction( value ); // 處理 WebKit 中出 checked if ( isFunction || ( l > 1 && typeof value === "string" && !support.checkClone && 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 ); } ); } if ( l ) { // 調用 buildFragment fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); // 第一個 elem first = fragment.firstChild; if ( fragment.childNodes.length === 1 ) { fragment = first; } // Require either new content or an interest in ignored elements to invoke the callback if ( first || ignored ) { scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); hasScripts = scripts.length; // Use the original fragment for the last item // instead of the first because it can end up // being emptied incorrectly in certain situations (#8070). for ( ; i < l; i++ ) { node = fragment; if ( i !== iNoClone ) { node = jQuery.clone( node, true, true ); // 克隆 scripts if ( hasScripts ) { // Support: Android <=4.0 only, PhantomJS 1 only // push.apply(_, arraylike) throws on ancient WebKit jQuery.merge( scripts, getAll( node, "script" ) ); } } // 回調,this 指向當前回調的 elem,這點很重要 // 很重要 callback.call( collection[ i ], node, i ); } // 這個 scripts 到底有什麼用,不懂 if ( hasScripts ) { doc = scripts[ scripts.length - 1 ].ownerDocument; // Reenable scripts jQuery.map( scripts, restoreScript ); // Evaluate executable scripts on first document insertion for ( i = 0; i < hasScripts; i++ ) { node = scripts[ i ]; if ( rscriptType.test( node.type || "" ) && !dataPriv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { if ( node.src ) { // Optional AJAX dependency, but won't run scripts if not present if ( jQuery._evalUrl ) { jQuery._evalUrl( node.src ); } } else { DOMEval( node.textContent.replace( rcleanScript, "" ), doc ); } } } } } } return collection; }
在我看來,domManip 主要的幾個功能包括:接受 HTML 字符串,並生成相對於的 dom,callback 回調函數,處理 dom,並且回調函數中的 this 是指向當前操做的 dom 的。剩下的事情,就交給回調函數去處理。
因此,domManip 的做用遠比想象的要少。
解密jQuery內核 DOM操做的核心函數domManip
解密jQuery內核 DOM操做的核心buildFragment
本文在 github 上的源碼地址,歡迎來 star。
歡迎來個人博客交流。