第八章:节点模块
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.js
https://github.com/RubyLouvre/mass-Framework/blob/master/node_fix.js
https://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对象里面包含的节点集合相同的个数,一个个插入。
<div>1</div> <div>2</div> <div>3</div> <div>4</div> <script type="text/javascript"> window.onload = function () { var a = document.createElement("span") a.innerHTML = "span"; $("div").append(a) } </script>
为了提高性能,合理利用高级api,mass的做法是能用createContextualFragment就用createContextualFragment.能用insertAdjacentHTML的就用insertAdjacentHTML,否则就转化为文档碎片,通过appendChild,insertBefore插入,这意味着里边分支会很复杂,我们需要搞个适配器,让它尽可能地分流。
至与API的命名,将沿袭jQuery的那几个名字,append,prepend,before,after与replace。值的一提的是,由于这几个方法太受欢迎,w3c在DOM4决定原生的支持它们。参数可以是字符串与DOM节点。
mass的这5个方法都是通过manipulate方法实现。
"append,prepend,before,after,replace".replace($.rword, function(method) { $.fn[method] = function(item) { return manipulation (this, method, item, this.ownerDocument); }; $.fn[method + "To"] = function(item) { $(item, this.ownerDocument) [method] (this); return this } })
mass的makeFragment函数,这里涉及到两个重要的知识点:NodeList的循环操作,文档碎片的复制。
NodeList看起来像数组,但它在插入节点时会立刻改变长度
<div id="test"> <a href="http://www.baidu.com/">link</a> </div> <script type="text/javascript"> window.onload = function () { var els = document.getElementsByTagName("a"); var div = document.getElementById("test"); for (var i = 0; i < els.length; i++) { var ele = document.createElement("a"); ele.setAttribute("href", "http://www.google.com/"); ele.appendChild(document.createTextNode("new link")); div.appendChild(ele);//添加一个新的链接 } } </script>
上面讲陷入死循环,我们在循环它时,我们最好将它的length保存到一个变量中, 然后比较是否中断循环。
第二个是碎片对象的复制问题,我们大可以使用原生的cloneNode(true),但在IE下,attachEvent绑定的事件会跟着被复制。由于不是我们框架绑定事件,那么再移除时就无法找到对应的引用了。
除此之外,jQuery还提供了wrap,wrapAll,wrappInner这三种特殊的插入操作。
wrap为当前元素提供了一个共同的父节点,此父节点将动态插入到远节点的父亲底下。这个我们可以轻松在IE下用neo.applyElement(old,"outside")实现。
wrapAll则是为一堆元素提供了一个共同的父节点,插入到第一个元素的父亲节点下,其它元素则统统挪到新节点底下。
wrapInner是为当前的元素插入一个新节点,然后将它之前的孩子挪到新节点底下,这个我们可以在IE下轻松用neo.applyElement(old,"inside")实现
这样看来,applyElement真是很强大,可以在标准浏览器扩展一下,让它应用的更广。
if (!document.documentElement.applyElement && typeof HTMLElement !== "undefined") { HTMLElement.prototype.removeNode = function(deep) { if (this.parentNode) { if (!deep) { var fragment; var range = this.ownerDocument.createRange(); range.selectNodeContents(this); fragment = range.extractContents(); range.setStartBefore(this); range.insertNode(fragment); range.detach() } return this.parentNode.removeChild(this); } if (!deep) { var range = this.ownerDocument.createRange(); range.selectNodeContents(this); range.deleteContents(); range.detach() } return this; } HTMLElement.prototype.applyElement = function(newNode, where) { newNode = newNode.removeNode(false); switch ((where || 'outside').toLowerCase()) { case 'inside' : var fragment; var range = this.ownerDocument.createRange(); range.selectNodeContents(this); range.surroundContents(newNode); range.detach(); beark; case 'outside' : var range = this.ownerDocument.createRange(); range.selectNode(this); range.surroundContents(newNode); range.detach(); beark; default : throw new Error ('DOMException.NOT_SUPPORTED_ERR(9)') } return newNode; } }
三.节点的复制
IE下对元素的复制与innerHTML一样,存在许多bug,非常著名的就是IE会多复制attachEvent事件。另外,根据测算,标准浏览器的cloneNode,只会复制元素写在标签内的属性与通过setAttribute设置的属性,而IE6-IE8还支持通过node.aaa = 'xxx'设置的属性复制。
如果是这样还好办,但IE在复制时不但会多复制一些,还会少复制一些,这让程序员不好处理。mass和jQuery一样,支持两个参数,第一个是复制节点,但不复制数据与事件。默认为false.第二个决定如何复制它的子孙,默认是遵循参数一。
$.fn.clone = function (dataAndEvent, deepDataAndEvents) { dataAndEvent = dataAndEvent == null ? false :dataAndEvents; deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; return this.map(function() { return cloneNode(this, dataAndEvents, deepDataAndEvents); }) }
可以看出,此方法只对参数进行处理,具体操作由cloneNode执行。
function cloneNode (node, dataAndEvents, deepDataAndEvents) { if (node.nodeType === 1) { var neo = $.fixCloneNode(node), //复制元素attibutes src, neos, i; if (dataAndEvents) { $.mergeData(neo, node); //复制数据与事件 if (deepDataAndEvents) {//处理子孙的复制 src = node[TAGS] ("*"); neos = neo[TAGS] ("*"); for (i = 0; src[i], i++) { $.mergeData(neos[i], src[i]); } } } src = neos = null; return neo; } else { return node.cloneNode(true); } }
cloneNode是做了分层设计的,如果在标准浏览器中fixCloneNode只是一个标准的cloneNode(true).关于更多fixCloneNode模块,请移步mass 的node_fix模块。详见fixNode函数。
这这个里,不得不提是mootools团队挖掘出来的mergeAttributes hack.早些年,为了在ie6-ie8中不复制attachEvent,jQuery被逼动用outerHTML来生成新的节点。
//应该是jquery 1.4 manipulation模块 clone : function (events) { var ret = this.map(function() { if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { var html = this.outerHTML, ownerDocument = this.ownerDocument; if (!html) { var div = ownerDocument.createElement("div"); div.appendChild( this.cloneNode(true) ); html = div.innerHTML; } return jQuery.clean([html.replace(rinlinejQuery, "") .replace(rleadingWhitespace, "")], ownerDocument)[0]; } else { return this.cloneNode(true) } }) //复制事件 if (events === true) { cloneCopyEvent( this, ret); cloneCopyEvent( this.find("*"), ret.find("*")); } return ret; }
即便这样,还是很臃肿,唯一的解决之道就是时间(等事件淘汰IE6等这些古老的浏览器,或者项目主管非常有魄力的支持新锐浏览器)。在zopto等手机框架。把这些数据都存储在data-*属性中。直接用一个cloneNode(true)就搞定节点的复制。
clone : function() { return this.map( function() { return this.cloneNode(true) } ) } //zepto
四.节点的移除
浏览器提供了很多节点移除的方法,常见的有removeChild、removeNode,动态创建一个元素节点或文档碎片,再appendChild,创建Range对象选中目标节点后,deleteContents。
removeNode是IE的私有实现,opera也实现了此方法。它的作用是将目标节点从文档树中删除,返回目标节点。它有一个参数,为布尔值,其默认值为false:即仅删除目标节点,保留子节点。true时同removeChild的用法。
deleteContents是比较偏门的API,兼容性差。
removeChild在IE6-7中存在内存泄露的问题,与IE的CG回收比较失败引起的。由于太底层,不展开说了。给出EXT的解决方案,像EXT这样庞大的UI库,所有的节点都是动态生成,因此是非常重视CG回收的。
var removeNode = IE6 || IE7 ? function() { var d ;//这里的ie6还ie7自己来实现 return function(node) { if (node && node.tagName != 'BODY') { d = d || document.createElement('DIV'); d.appendChild(node); d.innerHTML = ''; } } }() : function(node) { if (node && node.parentNode && node.tagName != 'BODY') { node.parentNode.removeChild(node); } }
为什么这么写,因为在IE6-8中存在一个叫DOM超空间(DOM hyperspace)的概念,当元素移出DOM树,但有javascript关联时元素不会消失,它被保留在一个叫超空间的地方。《PPK读javascript》一书中指出,可以用是否存在parentNode来判定元素是否才超空间。
window.onload = function () { var div = document.createElement("div"); alert(div.parentNode) //null document.body.removeChild(document.appendChild(div)); alert(div.parentNode); //ie6-8 object,其它的为null if (div.parentNode) { alert (div.parentNode.nodeType); // => 11 文档碎片 } }
上一个的alert出null , 这个所有的浏览器都一样,因此有时我们误以为可以当做节点是否在DOM的基准。但当元素插入DOM树再移出时,就有差异了。ie6-ie8弹出的是一个文档碎片。因此,可以想象ie的性能为什么这么差了,ie为自以为这样可以重复使用元素,但通常用户移出了就不管,因此,久而久之,内存允许了这么多碎片。加之其他的问题,就很容易造成泄露
我们看一下,innerHTML清除元素会怎么样。
<body><div id="test"></div></body> <script type="text/javascript"> window.onload = function() { var div = document.getElementById('test'); document.body.innerHTML = ''; alert(div.parentNode); //null } </script>
结果在IE下也是null,但这也不能说明innerHTML就比removeChild好。我们继续来一个实验
window.onload = function() { var div1 = document.getElementById('test1'); div1.parentNode.removeChild(div1); alert(div1.id + ":" + div1.innerHTML); // test1:test1 var div2 = document.getElementById('test2'); div2.parentNode.innerHTML = ""; alert(div2.id + ":" + div2.innerHTML); //(ie)test2: }
这时我们就发现,当用removeChild移出节点时,原来的元素结构没有发生变化,但在innerHTML时,ie6-ie8会直接清空里边的内容,只剩下空壳。而标准浏览器则与removeChild保持一致。
打个比喻,IE下,removeChild掰断树枝,但树枝可以再次使用。而innerHTML就是把所需树枝拔下来烧掉。鉴于IE下内存管理这么失败,能这么干净的清除节点正是我们寻找的方法!所有EXT从1.0到4.0,此方法没有什么大改变。
对于jQuery这样的框架类库来说。估计很难走这条路,它已经被自己的缓存系统绑架了(移除节点时需要逐个监测元素,从缓存系统中逐个移除对于的缓冲体,否则会造成浏览器宕机)。不过最不好的是,jQuery通过类数组结构与preObject困住节点的方式,造成了jQuery即便是使用innerHTML,元素节点在IE下还是位于DOM超空间中。
jQuery在性能上没有优势,于是在移除节点上造势 。它提供了三种移除节点的方式。remove,移除节点的同时从数据缓存上移除对应的数据。empty,只清空元素的内部,相当于IE下的removeChild(false)。detach,移除节点但不清除数据。前两种好理解。但为什么要创建第deatch方法呢?
从我们的工作业务看,DOM操作远不止这些,还有UI交互,样式渲染等。但后者都是基于前者上运作。
纯粹的javascript操作不会带来什么消耗,95%以上能耗是DOM操作引起的。出于性能考虑,我们最佳的做法是在设置样式前,将元素移出DOM树,处理完再插回来。
但绝大多数操作DOM的方法都与数据缓存方法关联在一起,若用remove方法,会让它们无法进行数据清理工作,导致内存泄露。而detach就是基于此需要而设计的。从设计理念来看,有点像数据库操作的事务。deatch开始一下,就开始一连串DOM操作,就算怎么操作,也不会伤及DOM树的其它元素。最后conmmit(append)一下,将最后的结果显示出来。
下面是实现过程
"remove, empty, detach".replace($.rword, function() { $.fn[method] = function() { var isRemove = method !== "empty"; for (var i = 0, node; node = this[i++];) { if (node.nodeType === 1) { //移除匹配操作 var array = $.slice(node[TGAS]("*")).concat(isRemove ? node : []); if (method !== "detach") { array.forEach(cleanNode); } } if (isRemove) { if (node.parentNode) { node.parentNode.removeChild(node); } } else { while (node.firstChild) { node.removeChild(node.firstChild); } } } return this; } });
如果我们的框架没有像jQuery那样引入庞大的数据缓存系统,而是像zopto.js那样通过h5的data-*来缓存数据,那么许多东西都可以简化了。这也意味着我们打算不兼容ie6 7 8,那么就可以使用deleteContens或textContent;
例如,我们事先一个清空元素内部的API:
方法1:
function clearChild (node) { //node可以是元素节点或文档碎片 while (node.firstChild) { node.removeChild(node.firstChild) } return node }
方法2,使用deleteContents,创建一个Range对象,然后通过setStartBefore,setEndAfter选择边界,最后清空它们的节点。
var deleteRange = document.createRange(); function clearChild(node) { //node可以是元素节点或碎片 deleteRange.setStartBefore(node.firstChild) deleteRange.setEndAfter(node.lastChild) deleteRange.deleteContents(); return node }
方法3:使用textContent .textContent是W3C版本中的innerText. 在较新的浏览器里兼容性特别好。并且同时存在于元素节点与文档碎片中。
function clearChild (node) { //node可以是元素节点或碎片 node.texrContent = ""; return node; }
五. innerHTML , innerText, outerHTML的处理
在开始之前,我们不得不审视一个问题。像innerHTML,innerText,outerHTML都是元素节点的一个属性,可读可写。由于我们的对象是一个类数组对象,所有操作都是集化操作,是不是每个方法都来一次for循环呢?正常的思路是getAll,setAll,类数组对象里有多少个元素节点,就处理多少次。如果有读操作,就返回一个数组,里边就返回一个数组,里边包含处理过的结果。mootools,YUI,EXT都采取这种策略。但jQuery选择一种奇特的策略,get first, set all。事实证明这个是成功的。如果返回一组结果,我们还要二次选择呢。
此外,jQuery大多数方法是动态方法,根据参数的情况有多种重载方式。如果每个这样的方法,都需要做这样那样的参数判定,显然很笨拙。但jQuery将它抽象成一个access方法。如果细读$.access方法,就全部掌握css,width,height,attr,prop,html,text,data等多态的用法了。
$.access = function (elems, callback, directive, args) { // 用于统一配置多态方法的读写访问 var length = elems.length, key = args[0], value = args[1];//读方法 if (args.length === 0 || args.length === 1 && typeof directive === "string") { var first = elems[0]; //由于只有一个回调,我们通过this == $判定读/写 return first && first.nodeType === 1 ? callback.call($, first, key) : viod 0; } else { //写方法 if (directive === null) { callback.call (elems, args); } else { if (typeof key === "object") { for (var k in key) { //为所有元素设置N个属性 for (var i = 0; i < length; i++) { callback.call(elems, elems[i], k, key[k]); } } } else { for (i = 0; i < length; i++) { callback.call(elems, elems[i], key, value); } } } } return elems; //返回自身 }
elems为要处理的节点的集合;callback为回调,里边有读操作与写操作,由this的情况决定进度入哪个分支;directive为处理指令,由于内部的分支很复杂,必须需要额外的flag进行分流。args,就是调用$.access函数的哪个函数的参数对象。
有了这个,我们来看如何实现操作innerHTML,innerText,outerHTML的方法。
html: function (item) { //取得或设置节点的innerHTML实现 return $.access(this, function(el, value) { if (this === $) { //getter return "innerHTML" in el ? el.innerHTML : innerHTML(el); } else { //setter value = item == null ? "" : item + ""; //如果item为null,undefined转换为空字符,其它强制转换字符串 //接着判断innerHTML属性是否符合标准,不再区分可读与只读 //用户传参是否包含了script style meta等不能用innerHTML直接进行创建的标签 //及像col td map legend 等需要满足嵌套关系才能创建的标签,否则会在IE与safari下报错 if ($.support.innerHTML && (!rcreate.test(value) && !return.test(value))) { try { for (var i = 0; el = this[i++];) { if (el.nodeType === 1) { if (el.nodeType === 1) { $.each(el[TAGS]("*"), cleanNode); el.innerHTML = value; } } return } catch (e) {} } this.empty().append(value); } }, null, arguments}) }, text : function(item) { //取得设置节点的text innerText textContent return $.access (this, function(el) { if (this === $) { //getter if (el.tagName === "SCRIPT") { return el.text; //ie6-8 只能用innerHTML text获取内容 } return el.textContent || el.innerText || $.getText([el]); } else { //setter this.empty().append(this.ownerDocument.createTextNode(item)); } }, null, arguments); } , outerHTML: function(item) { //设置或取得节点的outerHTML return $.access(this, function(el) { if (this === $) { //getter return "outerHTML" in el ? el.outerHTML : outerHTML(el); } else { //setter this.empty().replace(item) } }, null ,arguments) }
为了兼容xml,我们又搞了以下方法:
function outerHTML(el) { //主要用于XML for (var i = 0, c, ret = []; c = el.childNodes[i++];) { ret.push(outerHTML(c)); } return ret.join("") } function getText() { //获取某个节点的文本,如果此节点为元素节点,则取其childNodes的所有文本 return function getText(nodes) { for (var i = 0, ret = "", node; node = nodes[i++]) { //处理得文本节点与CDATA的内容 if (node.nodeType === 3 || node.nodeType === 4) { ret += node.nodeValue; //取得元素节点的内容 } else if (node.nodeType !== 8) { ret += getText(node.childNodes); } } return ret; } }()
实现一个完美的方法十分不容易。要做出各种权衡。各种妥协,能满足我们的业务需要就行了。
六. 一些奇葩的元素节点
即使我们的框架再大,总用覆盖不到的地方。比如IE的select标签移到遮罩层的上面来。XML数据岛提供另外一种文档套文档的方式。option无法通过css让它拥有更好看的样式。noscript取不到里边的innerHTML...浏览器有太多这样的细节。若无关紧要,框架的核心就是尽可能的忽略它。转交插件去处理。
到目前为止,我们只侧重照顾三个元素
1.iframe
iframe标签是一个古老的标签,IE3时已经存在了。由于它是用于镶嵌另外一个页面的到主页面,因此,肯定与一般的元素不同。创建起来也不是一般的消耗资源,并消耗连接数。但是它是一个物超所值的东西,有了它,我们就可以无障碍的实现无缝刷新,通过保存历史模拟onhashchange,安全的加载第三方资源与广告。实现富文本编辑器。文件上传。用它搞定IE6-7的selectbug,在iframe里做特征侦测。H5给它增加了3个属性,让它变得更强大。
由于是出自iframe,因此避免不了兼容性问题。首先是样式相关的。
想要隐藏iframe很粗的边框时,使用frameBorder属性,可以生成一下代码:
<iframe src="" frameborder="0"></iframe>
但是动态创建时,标准浏览器可以使用setAttribute来设置它。这时,作为一个特性,大小写不敏感。但老的ie不认。
var iframe = document.createElement("iframe"); iframe.setAttribute('frameborder',0);//ff有效 ,ie下无效。 iframe.frameBorder = 0;//唯有直接赋值时,双方才认,这个属性相当于css的border:0 iframe.scrolling = "no" ;//去掉滚动条
IE下想设置透明比较麻烦,并且在IE5.5才支持iframe内容透明,需要它透明,需要满足两个条件。
1.iframe的自身设置allowTransParency属性为true(但设置了allowTransparency = true就遮不住select)
2.iframe中的文档background-color或body元素的bgColor属性必须设置为tansparent.具体的例子如下:
1包含iframe页面的代码
<body bgcolor="#eeeeee"> <iframe src="" frameborder="0" allowTransparency = "true"></iframe> </body>
2.iframe页面
<body bgcolor="transparent"></body>
获取iframe 的window对象:
function getIframeWindow (node) { return node.contentWindow; }
我们也可以使用frames[iframeName]来取得它的window对象,这个所有浏览器都支持 ,由于iE, ID和NAME不怎么区分。因此,它也可以用frames[iframeId]来取
//取得iframe中的文档对象 function getIframeDocument(node) { //w3c || IE return node.contentDocument || node.contentWindow.document; }
判定页面是否在iframe里边
window.onload = function () { alert(window != window.top) alert(window.iframeElement !== null); alert(window.eval !== top.eval) }
判定iframe是否加载完毕
if (iframe.addEventListener) { iframe.addEventListener("load", callback, false); } else { iframe.attachEvent ("onload", callback) }
不过动态创建iframe,webkit浏览器可能出现二次触发onload事件的问题。
<div id="times"></div> <script type="text/javascript"> window.onload = function() { var c = document.getElementById("times"); var iframe = document.createElement("iframe"); iframe.onload = function() {c.innerHTML = ++c.innerHTML} document.body.append(iframe); iframe.src="http://baidu.com" } </script>
估计Safari和chorme在appendChild之后就进行一次加载,并且在设置src之前加载完毕,所以触发了两次,如果在body之前给iframe随便设置一个src(除了空值),间接加长第一次加载,那么也只触发一次。不设置src或空值的src相当于链接到了"about:blank"
动态加载iframe时,如果想用到name属性,就用document.createElement("iframe")创建在设置它的name属性,ie6 7是无法辨识此值的。
window.onload = function() { var iframe = document.createElement("iframe"); iframe.name = "xxx"; document.body.appendChild("iframe"); iframe.src = "http://www.baidu.com"; console.log(frames['xxx']); // undefined console.log(document.getElementsByName("xxx").length) ;//0 }
征对ie6, ie7,使用ie特有的创建元素时连属性一起创建的的方法实现
if ("1"[0]) { //IE6 ie7这里返回undefined,于是跑到第二个分支 var iframe = document.createElement("iframe"); iframe.name = name; } else { iframe = document.createElement('<iframe name="' + name + '">'); }
iframe与父窗口共享history,基于它我们可以解决Ajax时的后退按钮问题。里边的属性非常多,这里就不展开了。github上有两个非常知名的项目,可以解决工作时绝大多数问题。
https://github.com/browserstate/history.js
https://github.com/devote/HTML5-History-API
清空iframe内容,不保留历史的写法。
iframeWindow.location.replace('about:blank')
ie6下的iframe.src="about:blank"在https协议下会出现问题,需要用javascript:false修正。虽然速度非常慢,详见下面的讨论
http://gemal.dk/blog/2005/01/27/iframe_without_src_attribute_on_https_in_internet_exploer/
iframe与父窗口之间通信,如果是同源,那么它们之间可以随便操作,如果不同源,就需要postMessage与各种hack!。 所谓同源,就是域名,协议,端口相同
判定iframe与父页面同源
function isSameOrigin(el) { var ret = false; try { !!el.contentWindow.location.href; } catch (e) {} return ret; }
有关javascript如何跨域往上的文章有一大堆,这里只着重介绍两个,postMessage和navigator。
postMessage是h5重要的方法之一,估计未来的跨域就靠他了。它在ie8与稍微新一点的标准浏览器中都支持,并且能跨大域。涉及到postMessage方法与message事件。有关它们的用法可以看MDN和MSDN就行。
<script type="text/javascript"> window.onmessage = function (e) { var event = e || window.event; try { console.log(event); console.log(event.data); console.log(event.data.aaa) } catch (e) {} event.source.postMessage("好的,已经收到你的消息了",event.origin) } </script> 测试h5的postMessage <iframe src="http://study.ahthw.com/" id="aaa"></iframe> </body>
iframe页面中,它首先发出请求和对主页面的消息进行相应。
if (window.postMessage) { window.parent.postMessage("测试h5的postMessage!","*") window.parent.postMessage({aaa:"传个对象试试","*"}) } window.onmessage = function(e) { var event = e || window.event; console.log(event); console.log(event.data) }
第二种就是利用ie6 ie7的navigator对象的跨大域漏洞,至少没有被封堵,与postMessage结合使用应该能满足95%跨域需求。
<!-- 主页面 --> <iframe id="aaa" src="http://study.ahthw.com/Index.html"></iframe> <script type="text/javascript"> navigator.log = function (msg) { //用于ie 6 7下没有控制台,我们把调试信息打印到页面 var div = document.createElement("div"); div.innerHTML = msg; document.body.appendChild(div) } navigator.a = function(msg) { navigator.log("这是父亲页面中的a方法:" + msg); } var iframe = document.getElementById("aaa"); iframe.attachEvent && iframe.attachEvent("onload", function(){ setInterval(function(){ window.navigator.b("xxxxxx"); },3200) }) </script>
iframe页面如下: <script type="text/javascript"> navigator.b = function(msg) { navigator.log("这是iframe中的b方法" + msg) } setInterval(function(){ window.navigator.a('YYYYY'); },3300) </script>
更多的不做介绍
https://github.com/jiayi2/MessengerJS 可关注此跨大域解决方案。
(本文完结)