共计 13272 个字符,预计需要花费 34 分钟才能阅读完成。
前言:这篇我们倒着讲
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:
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>
(完)