前言:這篇我們倒著講
1、有這樣一個頁面:
<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>)
2、像之前的文章一樣,我們自定義 append() 方法
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);
}
}
)
複製程式碼
3、可以看到,append() 內部呼叫了 domManip 的方法,接下來重點介紹下該方法
(1)什麼是 domManip ?
domManip() 是 jQuery DOM 的核心函式。dom 即 Dom 元素,Manip 是Manipulate 的縮寫,連在一起就是 Dom 操作的意思。
(2)它的作用是?
domManip() 是用來處理 $().append(xxx)
、$().after(xxx)
等操作 DOM 方法的引數的,統一將其處理為 DOM 型別節點,並交由 callback 函式處理,即上圖的 callbackOne
。
注意: 本文暫不考慮引數包含 <script>
的情況,如:
ajQuery.append(innerArr,"
<script>
alert('append執行script')")
4、domManip() 的三個引數:nodelist, arguments, callbackOne
nodelist:即 document.querySelectorAll(".inner")
arguments:即字串 '<tr><td>test1</td><tr>'
callbackOne:回撥函式,在 nodelist、arguments 被相應邏輯處理後會返回一個文件碎片documentFragment,該方法會對 該文件碎片進行處理
注意:domMainp 函式講解在 第 8 點。
5、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 );
}
}
複製程式碼
6、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
}
複製程式碼
7、manipulationTarget() 中的函式:nodeName()
作用:
判斷兩個引數的nodename是否相等
原始碼:
//原始碼2843行-2847行
//判斷兩個引數的nodename是否相等
function nodeName( selector, name ) {
return selector.nodeName && selector.nodeName.toLowerCase() === name.toLowerCase();
}
複製程式碼
8、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
9、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
10、rtagName
作用:
匹配div不支援的標籤,如 tr、td等。
原始碼:
let rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i )
複製程式碼
11、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;
複製程式碼
12、jQuery.htmlPrefilter()
作用:
標籤轉換為閉合標籤,如<table>
--><table></table>
原始碼:
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
}
複製程式碼
13、綜上,當我呼叫了$('.inner').append('<tr><td>test1</td></tr>')
後,jQuery內部發生的事件如下
14、本篇文章的所有程式碼
github:
程式碼:
<!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>
複製程式碼
(完)