前言:這篇咱們倒着講html
一、有這樣一個頁面:node
<body> <button id="test1">append操做</button> <table class="inner"> <tbody></tbody> </table> </body>
<script> $('#test1').click(function(){ let innerArr=document.querySelectorAll(".inner") ajQuery.append(innerArr,'<tr><td>test1</td></tr>') }) </script>
注意:不要 append(<tr>test1</tr>)
,規範寫法是 append(<tr><td>test1</td></tr>)
git
二、像以前的文章同樣,咱們自定義 append() 方法github
let ajQuery={} jQuery.each({ //例:'<p>Test</p>' //源碼6011行-6019行 // 在被選元素的結尾插入指定內容 /*append的內部的原理,就是經過建立一個文檔碎片,把新增的節點放到文檔碎片中,經過文檔碎片克隆到到頁面上去,目的是效率更高*/ append: function(nodelist, arguments) { //node是由domManip處理獲得的文檔碎片documentFragment,裏面包含要插入的DOM節點 let callbackOne=function( node ) { console.log(node,'node149') //this指的就是$("xxx") //1:元素節點,11:DocumentFragment,9:document if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { //table插入tr的額外判斷 //target默認狀況是selector,即document.querySelectorAll(".inner") let target = manipulationTarget( this, node ) console.log(target,node.childNodes,'node147') //append的本質即便用原生appendChild方法在被選元素內部的結尾插入指定內容 target.appendChild( node ); } } console.log(nodelist,arguments,'this120') return domManip( nodelist, arguments, callbackOne ); }, }, function(key, value) { ajQuery[key] = function(nodelist, arguments) { console.log(nodelist,'nodelist128') return value(nodelist, arguments); } } )
三、能夠看到,append() 內部調用了 domManip 的方法,接下來重點介紹下該方法數組
(1)什麼是 domManip ?瀏覽器
domManip() 是 jQuery DOM 的核心函數。dom 即 Dom 元素,Manip 是Manipulate 的縮寫,連在一塊兒就是 Dom 操做的意思。app
(2)它的做用是?dom
domManip() 是用來處理 $().append(xxx)
、$().after(xxx)
等操做 DOM 方法的參數的,統一將其處理爲 DOM 類型節點,並交由 callback 函數處理,即上圖的 callbackOne
。函數
注意: 本文暫不考慮參數包含 <script>
的狀況,如:ui
ajQuery.append(innerArr,"
<script>
alert('append執行script')")
四、domManip() 的三個參數:nodelist, arguments, callbackOne
nodelist:即 document.querySelectorAll(".inner")
arguments:即字符串 '<tr><td>test1</td><tr>'
callbackOne:回調函數,在 nodelist、arguments 被相應邏輯處理後會返回一個文檔碎片documentFragment,該方法會對 該文檔碎片進行處理
注意:domMainp 函數講解在 第 8 點。
五、callbackOne()
做用:
將 domManip 返回的 documentFragment 插入到 selector 的內部末尾。
也就是說 $().append()
的本質是 DOM節點.appendChild(處理過的documentFragment(裏面包含插入的DOM節點))
源碼:
//node是由domManip處理獲得的文檔碎片documentFragment,裏面包含要插入的DOM節點 let callbackOne=function( node ) { console.log(node,'node149') //this指的就是$("xxx") //1:元素節點,11:DocumentFragment,9:document if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { //table插入tr的額外判斷 //target默認狀況是selector,即document.querySelectorAll(".inner") let target = manipulationTarget( this, node ) console.log(target,node.childNodes,'node147') //append的本質即便用原生appendChild方法在被選元素內部的結尾插入指定內容 target.appendChild( node ); } }
六、callbackOne() 中的函數:manipulationTarget()
做用:
額外判斷,當選擇器是table,而且插入的元素是tr時,會查找到table下的tbody,並返回tbody
源碼:
//源碼5724行-5733行 //額外判斷,當選擇器是table,而且插入的元素是tr時,會查找到table下的tbody,並返回tbody //this, node function manipulationTarget( selector, node ) { console.log(node.childNodes,node.firstChild,'node73') // 若是是table裏面插入行tr if ( nodeName( selector, "table" ) && nodeName( node.nodeType !== 11 ? node : node.firstChild, "tr" ) ) { return jQuery( selector ).children( "tbody" )[ 0 ] || selector } return selector }
七、manipulationTarget() 中的函數:nodeName()
做用:
判斷兩個參數的nodename是否相等
源碼:
//源碼2843行-2847行 //判斷兩個參數的nodename是否相等 function nodeName( selector, name ) { return selector.nodeName && selector.nodeName.toLowerCase() === name.toLowerCase(); }
八、jQueryDOM 核心函數:domManip()
做用:
將傳入的參數(dom節點元素、字符串、函數)統一轉化爲符合要求的DOM節點
源碼:
//源碼5597行-5586行 //做用是將傳入的參數(dom節點元素、字符串、函數)統一轉化爲符合要求的DOM節點 //例:$('.inner').append('<tr><td>Test</td></tr>') //nodelist即$('.inner') //args即<tr><td>Test</td></tr> function domManip( nodelist, args, callback ) { console.log(nodelist,args,'ignored5798') //數組深複製成新的數組副本 //源碼是:args = concat.apply( [], args ),這裏沒有用arguments,而是傳參就改了 let argsArr = [] argsArr.push(args) console.log(argsArr,'args31') //l 長度,好比類名爲.inner的li有兩組,2 let fragment, first, node, i = 0, //l 長度,好比類名爲.inner的li有兩組,2 l = nodelist.length, iNoClone = l - 1 //l=2 console.log(l,'lll45') if ( l ) { console.log(argsArr,nodelist[0].ownerDocument,nodelist,'firstChild40') //argsArr:<tr><td>test1</td></tr> //nodelist[0].ownerDocument:目標節點所屬的文檔 fragment = buildFragment(argsArr,nodelist[0].ownerDocument,false,nodelist ); first=fragment.firstChild console.log(fragment.childNodes,'firstChild42') //即<tr><td>test1</td></tr> if (first) { //=====根據nodelist的長度循環操做======== for ( ; i < l; i++ ) { console.log(node,fragment.childNodes,'childNodes49') node = fragment; if ( i !== iNoClone ) { /*createDocumentFragment建立的元素是一次性的,添加以後就不能再操做了, 因此須要克隆iNoClone的多個節點*/ node = jQuery.clone( node, true, true ); } console.log(nodelist[i], node.childNodes,'node50') //call(this,param) callback.call( nodelist[i], node); } //==================== } } console.log(nodelist,'nodelist58') return nodelist }
解析:
咱們能夠看到在 目標節點的個數 >=1 的狀況下(if(l){xxx}
),
調用了 buildFragment() 方法,該方法做用是 建立文檔碎片documentFragment,以便高效地向 目標節點 插入元素,而後根據 目標節點個數 循環地調用 callback 方法,即調用 原生 appendChild 方法插入元素。
**注意:
因爲 createDocumentFragment 建立的元素是一次性的,添加以後就成只讀的了,因此須要克隆 createDocumentFragment建立的元素,以便再次操做。**
關於 documentFragment,請看文章: jQuery之documentFragment
九、domManip() 中的函數 buildFragment()
做用:
建立文檔碎片
源碼:
//源碼4857行-4945行 /*建立文檔碎片,緣由是通常狀況下,咱們向DOM中添加新的元素或者節點,DOM會馬上更新。 若是向DOM添加100個節點,那麼就得更新100次,很是浪費瀏覽器資源。 解決辦法就是:咱們能夠建立一個文檔碎片(documentFragment), documentFragment相似於一個小的DOM,在它上面使用innerHTML並在innerHTML上插入多個節點,速度要快於DOM(2-10倍), 好比:先將新添加的100個節點添加到文檔碎片的innerHTML上,再將文檔碎片添加到DOM上。*/ //args, collection[ 0 ].ownerDocument, false, collection function buildFragment( arr, context, truefalse, selection ) { let elem,tmp, nodes = [], i = 0, l = arr.length,wrap,tag,j // createdocumentfragment()方法建立了一虛擬的節點對象,節點對象包含全部屬性和方法。 //至關於document.createDocumentFragment() let fragment = context.createDocumentFragment() //l=1 console.log(l,'l87') //============== for ( ; i < l; i++ ) { //'<tr><td></td></tr>' elem = arr[ i ]; console.log(i,elem,'elem90') if ( elem || elem === 0 ) { /*建立div是爲了處理innerHTML的缺陷(IE會忽略開頭的無做用域元素), 讓全部的元素都被div元素給包含起來,包括script,style等無做用域的元素*/ tmp=fragment.appendChild( context.createElement( "div" ) ) //就是匹配div不支持的標籤,如 tr、td等 /*不支持innerHTML屬性的元素,經過正則單獨取出處理*/ tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); /*做用就是利用wrapMap讓不支持innerHTML的元素經過包裝wrap來支持innerHTML*/ //ie對字符串進行trimLeft操做,其他是用戶輸入處理 //不少標籤不能單獨做爲DIV的子元素 /*td,th,tr,tfoot,tbody等等,須要加頭尾*/ wrap = wrapMap[ tag ] || wrapMap._default // tr: [ 2, "<table><tbody>", "</tbody></table>" ] console.log(wrap,'wrap152') //將修正好的element添加進innerHTML中 //jQuery.htmlPrefilter:標籤轉換爲閉合標籤,如<table> --> <table></table> /*div不支持tr、td因此須要添加頭尾標籤,如<div><table><tbody>xxxx</tbody></table>*/ tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; // 由於warp被包裝過,須要找到正確的元素父級 j = wrap[ 0 ]; //2 while ( j-- ) { tmp = tmp.lastChild; } //temp:<tbody></tbody> //tmp.childNodes:tr //nodes:[] //jQuery.merge:將兩個數組合併到第一個數組中 jQuery.merge( nodes, tmp.childNodes ); } } //================ // Remove wrapper from fragment fragment.textContent = ""; //須要將i重置爲0 i=0 while ( ( elem = nodes[ i++ ] ) ) { fragment.appendChild( elem ) } console.log(fragment.childNodes,'fragment105') return fragment; }
解析:
(1)建立文檔碎片 documentFragment
let fragment = context.createDocumentFragment()
(2)在 待插入的元素存在的狀況下,先在 documentFragment 內部插入 <div></div>
標籤
建立div是爲了處理innerHTML的缺陷(IE會忽略開頭的無做用域元素),因此讓全部的元素都被div元素給包含起來,包括script,style等無做用域的元素
(3)可是 <div>
也有不支持的子元素,經過 wrap
篩選幷包裝這些子元素
好比<tr>
標籤,會被 wrap
轉爲 <table><tbody></tbody></table>
,再成功添加到 documentFragment 的 innerHTML 中。
(4)documentFragment 在成功添加完子元素後,再卸磨殺驢,去掉包裹的節點,如上例的<div><table><tbody></tbody></table></div>
,保留待插入的節點<tr><td>test1</td></tr>
(5)最後返回 處理好的文檔碎片 fragment
十、rtagName
做用:
匹配div不支持的標籤,如 tr、td等。
源碼:
let rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i )
十一、wrapMap
做用:
div 不支持的標籤表
源碼:
let wrapMap = { // Support: IE <=9 only option: [ 1, "<select multiple='multiple'>", "</select>" ], // XHTML parsers do not magically insert elements in the // same way that tag soup parsers do. So we cannot shorten // this by omitting <tbody> or other required elements. thead: [ 1, "<table>", "</table>" ], col: [ 2, "<table><colgroup>", "</colgroup></table>" ], tr: [ 2, "<table><tbody>", "</tbody></table>" ], td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], _default: [ 0, "", "" ] }; // Support: IE <=9 only wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td;
十二、jQuery.htmlPrefilter()
做用:
標籤轉換爲閉合標籤,如<table>
--><table></table>
源碼:
htmlPrefilter: function( html ) { return html.replace( rxhtmlTag, "<$1></$2>" ); }
1三、綜上,當我調用了$('.inner').append('<tr><td>test1</td></tr>')
後,jQuery內部發生的事件以下
1四、本篇文章的全部代碼
github:
https://github.com/AttackXiao...
代碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>jQuery的遍歷結構設計之節點操做</title> </head> <body> <script src="jQuery.js"></script> <button id="test1">append操做</button> <table class="inner"> <!--<tbody></tbody>--> </table> <script> //匹配div不支持的標籤,如 tr、td等 let rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i ); //================================ let wrapMap = { // Support: IE <=9 only option: [ 1, "<select multiple='multiple'>", "</select>" ], // XHTML parsers do not magically insert elements in the // same way that tag soup parsers do. So we cannot shorten // this by omitting <tbody> or other required elements. thead: [ 1, "<table>", "</table>" ], col: [ 2, "<table><colgroup>", "</colgroup></table>" ], tr: [ 2, "<table><tbody>", "</tbody></table>" ], td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], _default: [ 0, "", "" ] }; // Support: IE <=9 only wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; //================================ //源碼5597行-5586行 //做用是將傳入的參數(dom節點元素、字符串、函數)統一轉化爲符合要求的DOM節點 //例:$('.inner').append('<tr><td>test1</tr></td>') //nodelist(collections)即$('.inner') //args即<tr><td>test1</td></tr> function domManip( nodelist, args, callback ) { console.log(nodelist,args,'ignored5798') //數組深複製成新的數組副本 //源碼是:args = concat.apply( [], args ),這裏沒有用arguments,而是傳參就改了 let argsArr = [] argsArr.push(args) console.log(argsArr,'args31') //l 長度,好比類名爲.inner的li有兩組,2 let fragment, first, node, i = 0, //l 長度,好比類名爲.inner的li有兩組,2 l = nodelist.length, iNoClone = l - 1 //l=2 console.log(l,'lll45') if ( l ) { console.log(argsArr,nodelist[0].ownerDocument,nodelist,'firstChild40') //argsArr:<p>Test</p> //nodelist[0].ownerDocument:目標節點所屬的文檔 fragment = buildFragment(argsArr,nodelist[0].ownerDocument,false,nodelist ); first=fragment.firstChild console.log(fragment.childNodes,'firstChild42') //即<p>Test</p> if (first) { //=====根據nodelist的長度循環操做======== for ( ; i < l; i++ ) { console.log(node,fragment.childNodes,'childNodes49') node = fragment; if ( i !== iNoClone ) { /*createDocumentFragment建立的元素是一次性的,添加以後再就不能操做了, 因此須要克隆iNoClone的多個節點*/ node = jQuery.clone( node, true, true ); } console.log(nodelist[i], node.childNodes,'node50') //call(this,param) callback.call( nodelist[i], node); } //==================== } } console.log(nodelist,'nodelist58') return nodelist } //源碼5724行-5733行 //額外判斷,當選擇器是table,而且插入的元素是tr時,會查找到table下的tbody,並返回tbody //this, node function manipulationTarget( selector, node ) { console.log(node.childNodes,node.firstChild,'node73') // 若是是table裏面插入行tr if ( nodeName( selector, "table" ) && nodeName( node.nodeType !== 11 ? node : node.firstChild, "tr" ) ) { return jQuery( selector ).children( "tbody" )[ 0 ] || selector } return selector } //源碼2843行-2847行 //判斷兩個參數的nodename是否相等 function nodeName( selector, name ) { return selector.nodeName && selector.nodeName.toLowerCase() === name.toLowerCase(); } //源碼4857行-4945行 /*建立文檔碎片,緣由是通常狀況下,咱們向DOM中添加新的元素或者節點,DOM會馬上更新。 若是向DOM添加100個節點,那麼就得更新100次,很是浪費瀏覽器資源。 解決辦法就是:咱們能夠建立一個文檔碎片(documentFragment), documentFragment相似於一個小的DOM,在它上面使用innerHTML並在innerHTML上插入多個節點,速度要快於DOM(2-10倍), 好比:先將新添加的100個節點添加到文檔碎片的innerHTML上,再將文檔碎片添加到DOM上。*/ //args, collection[ 0 ].ownerDocument, false, collection function buildFragment( arr, context, truefalse, selection ) { let elem,tmp, nodes = [], i = 0, l = arr.length,wrap,tag,j // createdocumentfragment()方法建立了一虛擬的節點對象,節點對象包含全部屬性和方法。 //至關於document.createDocumentFragment() let fragment = context.createDocumentFragment() //l=1 console.log(l,'l87') //============== for ( ; i < l; i++ ) { //'<tr><td></td></tr>' elem = arr[ i ]; console.log(i,elem,'elem90') if ( elem || elem === 0 ) { /*建立div是爲了處理innerHTML的缺陷(IE會忽略開頭的無做用域元素), 讓全部的元素都被div元素給包含起來,包括script,style等無做用域的元素*/ tmp=fragment.appendChild( context.createElement( "div" ) ) //就是匹配div不支持的標籤,如 tr、td等 /*不支持innerHTML屬性的元素,經過正則單獨取出處理*/ tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); /*做用就是利用wrapMap讓不支持innerHTML的元素經過包裝wrap來支持innerHTML*/ //ie對字符串進行trimLeft操做,其他是用戶輸入處理 //不少標籤不能單獨做爲DIV的子元素 /*td,th,tr,tfoot,tbody等等,須要加頭尾*/ wrap = wrapMap[ tag ] || wrapMap._default // tr: [ 2, "<table><tbody>", "</tbody></table>" ] console.log(wrap,'wrap152') //將修正好的element添加進innerHTML中 //jQuery.htmlPrefilter:標籤轉換爲閉合標籤,如<table> --> <table></table> /*div不支持tr、td因此須要添加頭尾標籤,如<div><table><tbody>xxxx</tbody></table>*/ tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; // 由於warp被包裝過,須要找到正確的元素父級 j = wrap[ 0 ]; //2 while ( j-- ) { tmp = tmp.lastChild; } //temp:<tbody></tbody> //tmp.childNodes:tr //nodes:[] //jQuery.merge:將兩個數組合併到第一個數組中 jQuery.merge( nodes, tmp.childNodes ); } } //================ // Remove wrapper from fragment fragment.textContent = ""; //須要將i重置爲0 i=0 while ( ( elem = nodes[ i++ ] ) ) { fragment.appendChild( elem ) } console.log(fragment.childNodes,'fragment105') return fragment; } let ajQuery={} jQuery.each({ //例:'<tr><td>test1</td></tr>' //源碼6011行-6019行 // 在被選元素的結尾插入指定內容 /*append的內部的原理,就是經過建立一個文檔碎片,把新增的節點放到文檔碎片中,經過文檔碎片克隆到到頁面上去,目的是效率更高*/ append: function(nodelist, arguments) { //node是由domManip處理獲得的文檔碎片documentFragment,裏面包含要插入的DOM節點 let callbackOne=function( node ) { console.log(node,'node149') //this指的就是$("xxx") //1:元素節點,11:DocumentFragment,9:document if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { //table插入tr的額外判斷 //target默認狀況是selector,即document.querySelectorAll(".inner") let target = manipulationTarget( this, node ) console.log(target,node.childNodes,'node147') //append的本質即便用原生appendChild方法在被選元素內部的結尾插入指定內容 target.appendChild( node ); } } console.log(nodelist,arguments,'this120') return domManip( nodelist, arguments, callbackOne ); }, }, function(key, value) { ajQuery[key] = function(nodelist, arguments) { console.log(nodelist,'nodelist128') return value(nodelist, arguments); } } ) $('#test1').click(function(){ let innerArr=document.querySelectorAll(".inner") ajQuery.append(innerArr,'<tr><td>test1</td></tr>') }) </script> </body> </html>
(完)