DOM节点操作占我们前端工作很大一部分,其节点的操作又占50%以上。由于选择器引擎的出现,让繁琐的元素选择简单化,并且一下子返回一大堆元素,这个情景时刻暗示着我们操作元素就像CSS为元素添加样式那样,一操作就操作一组元素。
一些大胆的API设计被提出来。当然我们认为时髦新颖的设计其实都是很久以前被忽略的设计或者其它领域的设计。例如:集化操作,这是数据库层里边的ROM就有的。链式操作,javascript对象基于原型的构造为它大开方便之门。信息密集的DSL,有rails这座高峰让大家崇拜。jQuery集众所长,让节点操作简单到极致。对于jQuery,每个方法重载的厉害。作为一个流行的产品,必须模仿着众。本章以mass框架的节点操作模块为主,其原因是mass分层做的比较好,node模块相当于jQuery2.0中的manipulation模块与traversing模块,node_fix则是兼容ie6-8的部分。(这里体现了AMD规范设计的优势)
我们来看DOM的操作包括哪些,CRUD(数据库所说的),C就是创建,在集化操作里,innerHTML可以满足我们一下创建多个节点的需求。
R,就是读取查找,如果解释查找的话,选择器引擎已经为我们做了,如果是读取,那么我们还可以将innerHTML,innerText,outerHTML这些属于元素内容的东西划归它处理
U,就是更新,innerHTML,innerText,outerHTML,这就出现一个问题,需要两个API来处理,还是合成一个。jQuery的RU结合的方式建议只用框架底层的API,到UI层面还是分开。底层这样做,可以保证API的数量少,使用门槛降低,高层的API,则数量非常庞杂,窗口、面板、跑马灯、拖放等都有一大堆方法,他们在每一个项目中调用的次数比底层API少多了。因此,我们不会专门去严谨,只有用到了才会去看。我们尽量做到语意化。java传统的getXXX setXXX addXXX removeXXX是我们的首选。
最后是D,移除,我们在jQuery中有3个API,remove detach empty,各有各的使用范围。因此,节点的操作是围着DOM树操作。既然是树,就有插入操作,jQuery划分为4个API,其实我们可以看做是IE的insertAdjacentXXX的强化版。还有clone,克隆允许,克隆缓存数据与事件。
本章围绕mass的node与node_fix模块,jQuery的manipulation模块。https://github.com/RubyLouvre/mass-Framework/blob/master/node.jshttps://github.com/RubyLouvre/mass-Framework/blob/master/node_fix.jshttps://github.com/jquery/jquery/blob/master/src/manipulation.js
一:节点的创建
浏览器提供了多种手段创建API。从流行度来看,依然是document.createElement,innerHTML,insertAdjacentHTML,createContextualFragment。
document.createElement基本不用说什么,它传入一个标签名,然后返回此类元素的节点。并且对于浏览器不支持的标签,也会返回(这也成为了ie6-ie8支持html5新标签的救命稻草)。ie6-ie8中,还有一种方法,能允许标签连同属性一起生成,比如document.createElement("<div id=aaa></div>"),此方法常见生生name属性的input与iframe元素。因为ie6-7下这两个元素只读,不允许修改。
function createNameElement(type, name) { var element = null; //尝试ie方式 try { element = document.createElement('<' + type + ' name="' + name + '">'); } catch (e) {} if (!element || element.nodeName != type.toUpperCase()) { // non -ie element = document.createElement(type); element.name = name; } return element; } createNameElement("dd","aa"); //=> <dd></dd> name = aa
innerHTML本来是IE的私有实现,在jQuery1.0就开始挖掘innerHTML了,这不但是innerHTML的创建效率比createElement快2-10倍不等,还因为innerHTML一下能生产一大堆节点。这与jQuery推崇的宗旨不谋而合,但innerHTML发生了兼容性的问题,比如IE会对用户字符串进行trimleft操作,本意是只能去除空白,但FF认为要忠于用户操作,单元位置要生成文本节点。
var div = document.createElement("div"); div.innerHTML = "<b>2</b><b>2</b>" console.log(div.childNodes.length); //ie6-8 3.其它浏览器4 console.log(div.firstChild.nodeType); //ie6 1,其它3
IE下有些元素的innerHTML是只读的,重写innerHTML会报错,这就导致我们在动态插入时,不能转求appendChild,insertBefore来处理:
来自MSDN:IE的innerHTML会忽略掉no-scope element.no-scope element是IE的内部概念,隐藏的很深。仅在MSDN说明注释节点是no-scope element,或在官方论坛中透露一点内容,script和style也是no-scope element。经过这么多年的挖掘,大致确认注释,style,script,link,meta,noscript表示功能性的标签为no-scope element,想要用innerHTML生成它们,必须在它们之前加上文字或其它标签。
//ie6 8 var div = document.createElement("div"); div.innerHTML = '<meta charset=utf-8 />'; //0 alert(div.childNodes.length) div.innerHTML = 'x<meta charset=utf-8 />'; //2 alert(div.childNodes.length)
另外,一个周知的问题是innerHTML不会执行script标签里的脚本(其实也不然,如果浏览器支持script标签的defer属性,它就能执行。这个特性比较难检测,因此,jQuery一类的直接用正则把它里边的内容抽取出来,直接全局eval了)。 mass的思路是,反正innerHTML赋值后已经将它们转换为节点了,那么再将它们抽取出来,再用document.createElement("script")生成的节点代替就行了。
最后,就是一些元素不能单独作为div的子元素,比如td, th元素,需要在最外边包裹几层,才能放到innerHTML中解释,否则浏览器就会将其当成普通文本节点生成。这个是jQuery团队发现的,现在所有框架都借用此技术生成节点。如果把这些标签比做是标签,那么孵化它们出来的父元素就是胎盘。在w3c规范中,它们都是这样一组组分成不同的模块。
胚胎 | 胎盘 |
area | map |
param | object |
col | tbody,table,colgroup |
legend | fieldset |
option,optground | select |
thead,tfoot,tbody,colgroup | table |
tr | table,tbody |
td,th | table,tbody,tr |
一直以前,人们都是使用完整的闭合标签来包裹这些特殊的标签,直到人们发现浏览器会自动补全闭合标签后。
var div = document.createElement("div"); div.innerHTML = '<table><tbody><tr></tr></tbody></table>'; alert(div.getElementsByTagName("tr").length); //1 div.innerHTML = '<table><tbody><tr>'; alert(div.getElementsByTagName("tr").length); //1
能自动补全的有body,colgrounp,dd,dt,head,html,li,optgroup,option,p,tbody,td,tfoot,th,thead,tr.在网速奇慢的年代是一个优化,也是吸引开发者到自己的阵营。
现在已经不推荐这样做,浏览器会固守规则,少写结束标签,很容易引起镶嵌错误,xhtml布道者就是抓住这一点死命抨击html4。
insertAdjacentHTML,也是IE的私有实现,DHTML的产物,比起其他的API,它具有灵活的插入方式。
(更多参考:http://www.cnblogs.com/ahthw/p/4309343.html)
他们一一对应jQuery的PRepend,append,before,after。因此,用它来构造这几个方法,代码量会大大减少。但是实现的过程要比我们想象的复杂,我们可以另外一个insertAdjacentHTML来搞定,
insertAdjacentHTML兼容情况如下所示:
浏览器 | chorme | FF | IE | Opera | Safari(webkit) |
版本 | 1 | 8 | 4 | 7 | 4(527) |
如果浏览器不支持insertAdjacentHTML,那么我们可以用下面介绍的crateContextualFragment来模拟(模拟函数略)。
createContextualFragment是FF推出来的私有实现,它是Range对象的一个实例方法,相当于insertAdjacentHTML直接将内容插入到DOM树,createContextualFragment则是允许我们将字符串转换为文档碎片,然后由你决定插入到哪里。
在著名的emberjs中,如果支持Range ,那么它的html,append,prepend,after等方法都用createContextualFragment与deleteContents实现。createContextualFragment和insertAdjacentHTML一样,要字符串遵循HTML的嵌套规则。
此外,我们还可以用document.write来创建内容,但我们动态添加节点时多发生在dom树建完之后,因此不太合适,这里就不展开了。
之后我们看看mass是怎么实现的,它的结构与jQuery一样,通过两个构造器与一个原型实现无new实例化,这样我们的链式操作就不会被new关键字打断。
function $(a, b) { //第一个构造器 return new $.fn.init(a, b); //第二个构造器 } //将原型对象放到一个名字更短更好记的属性名中 //这是jQuery人性化的体现,也方便扩展原型方法 $.fn = $.prototype = { init : function (a, b) { this.a = a; this.b = b; } } //共用一个原型 $.fn.init.prototype = $.fn; var a = $(1, 2) console.log(a instanceof $); console.log(a instanceof $.fn.init)
上面的这个结构非常重要,所有jQuery风格的类库框架都沿袭它实现的链式操作
根据jQuery官方的介绍,它包含9种不同的传参方法。
jQuery(selector[,context])jQuery(element)jQuery(elementArray)jQuery(object)jQuery(jQuery object)jQuery()jQuery(html,[ownerDocument])jQuery(html,attributes)jQuery(callback)
若按功能来分,它大致分为3种:选择器,domparser与domReady。
由于重载的太多了,因此基本上号称jQuery-compatible的类库框架都没有实现它所有重载。如果抛开这些细节,我们不难发现,除了最后的domReady,其它一切目的不过是想获取要操作的节点罢了。为了更方便的操作,这些节点与实例通过数字进行并联,构成一个类数组对象,因此,你会看到它绑定了push,unshift,pop,shift,splice,sort,reverse,each,map等数组方法,让它看起来就是一个数组。
labor相当于jQuery的pushStack,用于构建下一个类数组对象,比如map,lt,gt,eq等方法就是内部调用它来实现,但jQuery的pushStack远没有这么简单,它还有一个prevObject属性,保存着上次操作的对象。链式操作越多,被引用不能释放的东西就越多,或者处于未来只能使用querySelectorAll做选择器的考量,它们都是好东西。工作业务中,只高亮表格偶数行这一需要也很频繁。因此,做成一个独立的方法是明智的选择。
mass而根据用户传入字符生成一堆节点功能则是由parseHTML方法实现的。parseHTML是一个复杂的方法,它对不同的浏览器做了分级处理,对于ie6-8,框架还会夹在node_fix模块,里边有fixparseHTML,为它打补丁(此处有征对ie6 8的修复方法)。
二.节点的插入
原生的DOM接口是非常简单的,参数类型确定,不会重载,每次只能处理一个元素节点;而jQuery式的方法则相反,虽然名字短,但参数类型复杂,过度重载,对于插入这样的写操作,是进行批处理的。
为了简化处理逻辑,jQuery的做法是统统转换为文档碎片,然后将它复制到与当前jQuery对象里面包含的节点集合相
新闻热点
疑难解答