DOM節點操做佔咱們前端工做很大一部分,其節點的操做又佔50%以上。因爲選擇器引擎的出現,讓繁瑣的元素選擇簡單化,而且一會兒返回一大堆元素,這個情景時刻暗示着咱們操做元素就像css爲元素添加樣式那樣,一操做就操做一組元素。javascript
一些大膽的API設計被提出來。固然咱們認爲時髦新穎的設計其實都是好久之前被忽略的設計或者其它領域的設計。例如:集化操做,這是數據庫層裏邊的ROM就有的。鏈式操做,javascript對象基於原型的構造爲它大開方便之門。信息密集的DSL,有rails這座高峯讓你們崇拜。jQuery集衆所長,讓節點操做簡單到極致。對於jQuery,每一個方法重載的厲害。做爲一個流行的產品,必須模仿着衆。本章以mass框架的節點操做模塊爲主,其緣由是mass分層作的比較好,node模塊至關於jQuery2.0中的manipulation模塊與traversing模塊,node_fix則是兼容ie6-8的部分。(這裏體現了AMD規範設計的優點)css
咱們來看DOM的操做包括哪些,CRUD(數據庫所說的),C就是建立,在集化操做裏,innerHTML能夠知足咱們一下建立多個節點的需求。html
R,就是讀取查找,若是解釋查找的話,選擇器引擎已經爲咱們作了,若是是讀取,那麼咱們還能夠將innerHTML,innerText,outerHTML這些屬於元素內容的東西劃歸它處理前端
U,就是更新,innerHTML,innerText,outerHTML,這就出現一個問題,須要兩個API來處理,仍是合成一個。jQuery的RU結合的方式建議只用框架底層的API,到UI層面仍是分開。底層這樣作,能夠保證API的數量少,使用門檻下降,高層的API,則數量很是龐雜,窗口、面板、跑馬燈、拖放等都有一大堆方法,他們在每個項目中調用的次數比底層API少多了。所以,咱們不會專門去嚴謹,只有用到了纔會去看。咱們儘可能作到語意化。java傳統的getXXX setXXX addXXX removeXXX是咱們的首選。html5
最後是D,移除,咱們在jQuery中有3個API,remove detach empty,各有各的使用範圍。所以,節點的操做是圍着DOM樹操做。既然是樹,就有插入操做,jQuery劃分爲4個API,其實咱們能夠看作是IE的insertAdjacentXXX的強化版。還有clone,克隆容許,克隆緩存數據與事件。java
本章圍繞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.jsnode
一:節點的建立jquery
瀏覽器提供了多種手段建立API。從流行度來看,依然是document.createElement,innerHTML,insertAdjacentHTML,createContextualFragment。git
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 可關注此跨大域解決方案。