dom也就是文檔對象模型,是針對HTML和XML的一個api,描繪了一個層次化的節點樹。雖然瀏覽器原生給咱們提供了許多操做dom的方法,使咱們能夠對dom進行查找,複製,替換和刪除等操做。可是zepto在其基礎上再次封裝,給以咱們更加便捷的操做方式。先看下圖,咱們以
刪除元素
,插入元素
,複製元素
,包裹元素
和替換元素
幾個模塊分別探究zepto如何一一將其實現。javascript
原文連接css
github項目地址html
當父節點存在時,從其父節點中刪除當前集合中的元素。java
remove: function () { return this.each(function () { if (this.parentNode != null) this.parentNode.removeChild(this) }) }
遍歷當前集合中的元素,當該元素的父節點存在的時候,使用removeChild
刪除該元素。node
功能和remove同樣,都是刪除元素。git
$.fn.detach = $.fn.remove
能夠看到就是在$的原型上添加了一個指向remove
函數的方法detach
。github
清空對象集合中每一個元素的DOM內容ajax
empty: function () { return this.each(function () { this.innerHTML = '' }) },
遍歷當前集合中的元素,而後將元素的innerHTML屬性設置爲空。也就達到了清除DOM內容的目的。json
插入元素的相關api比較多,咱們先來重溫部分api的使用用法和比較一下他們之間的區別。api
<ul class="box"> <li>1</li> </ul>
let $box = $('.box') let insertDom = '<li>i am child</li>' // append appendTo // $box.append(insertDom) // $(insertDom).appendTo($box) /* <ul class="box"> <li>1</li> <li>i am child</li> </ul> */ // prepend prependTo // $box.prepend(insertDom) // $(insertDom).prependTo($box) /* <ul class="box"> <li>i am child</li> <li>1</li> </ul> */ // before insertBefore // $box.before(insertDom) // $(insertDom).insertBefore($box) /* <li>i am child</li> <ul class="box"> <li>1</li> </ul> */ // after insertAfter // $box.after(insertDom) // $(insertDom).insertAfter($box) /* <ul class="box"> <li>1</li> </ul> <li>i am child</li> */
以上是append
,appendTo
,prepend
,prependTo
,after
,insertAfter
,before
,insertBefore
八個方法的基本用法,以及用過以後的dom結構。咱們總結一下他們的區別。
首先每一個方法的入參均可覺得html字符串,dom節點,或者節點組成的數組。參考自zeptojs_api
append
,appendTo
,prepend
,prependTo
都是在元素內部插入內容,而after
,insertAfter
,before
,insertBefore
則是在元素外部插入內容。
append
,appendTo
是在元素的末尾插入內容,prepend
,prependTo
是在元素的初始位置插入,after
,insertAfter
是在元素的後面插入內容,before
,insertBefore
則是在元素的前面插入內容
接下來咱們開始學習和閱讀實現這8大方法的核心源碼部分
adjacencyOperators = ['after', 'prepend', 'before', 'append'] adjacencyOperators.forEach(function(operator, operatorIndex) { var inside = operatorIndex % 2 $.fn[operator] = function() { // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings var argType, nodes = $.map(arguments, function(arg) { var arr = [] argType = type(arg) if (argType == "array") { arg.forEach(function(el) { if (el.nodeType !== undefined) return arr.push(el) else if ($.zepto.isZ(el)) return arr = arr.concat(el.get()) arr = arr.concat(zepto.fragment(el)) }) return arr } return argType == "object" || arg == null ? arg : zepto.fragment(arg) }), parent, copyByClone = this.length > 1 if (nodes.length < 1) return this return this.each(function(_, target) { parent = inside ? target : target.parentNode // convert all methods to a "before" operation target = operatorIndex == 0 ? target.nextSibling : operatorIndex == 1 ? target.firstChild : operatorIndex == 2 ? target : null var parentInDocument = $.contains(document.documentElement, parent) nodes.forEach(function(node) { if (copyByClone) node = node.cloneNode(true) else if (!parent) return $(node).remove() parent.insertBefore(node, target) if (parentInDocument) traverseNode(node, function(el) { if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && (!el.type || el.type === 'text/javascript') && !el.src) { var target = el.ownerDocument ? el.ownerDocument.defaultView : window target['eval'].call(target, el.innerHTML) } }) }) }) }
遍歷adjacencyOperators數組給$原型添加對應的方法
adjacencyOperators = ['after', 'prepend', 'before', 'append'] adjacencyOperators.forEach(function(operator, operatorIndex) { // xxx $.fn[operator] = function() { // xxx } // xxx })
能夠看到經過循環遍歷adjacencyOperators
從而給$的原型添加對應的方法。
轉換node節點
var argType, nodes = $.map(arguments, function(arg) { var arr = [] argType = type(arg) if (argType == "array") { arg.forEach(function(el) { if (el.nodeType !== undefined) return arr.push(el) else if ($.zepto.isZ(el)) return arr = arr.concat(el.get()) arr = arr.concat(zepto.fragment(el)) }) return arr } return argType == "object" || arg == null ? arg : zepto.fragment(arg) })
例子
// 1 html字符串 $box.append('<span>hello world</span>') // 2 dom節點 $box.append(document.createElement('span')) // 3 多個參數 $box.append('<span>1</span>', '<span>2</span>') // 4 數組 $box.append(['<span>hello world</span>', document.createElement('span')])
由於傳入的內容能夠爲html字符串,dom節點,或者節點組成的數組。這裏對可能的狀況分類型作了處理。經過內部的type
函數判斷每一個參數的數據類型並保存在argType
中。
當參數類型爲數組(相似上面例子中的4)的時候,再對該參數進行遍歷,若是該參數中的元素存在nodeType
屬性則將該元素推動數組arr,
若是該參數中的元素是一個Zepto對象
,則調用get方法,將arr與返回的原生元素數組進行合併。
當參數類型爲object
或者null
的時候直接返回,不然就是處理字符串形式了,經過調用zepto.fragment(這個函數在後面的文章中會詳細講解,如今就其理解爲將html字符串處理成dom節點數組就能夠了)處理並將結果返回。
到如今爲止,咱們已經明白了怎麼將傳入的content
轉化爲對應的dom節點
。
接下來咱們來看如何將nodes
中建立好的dom節點插入到目標位置。
parent, copyByClone = this.length > 1 if (nodes.length < 1) return this
先留意一下parent
,以及copyByClone
這兩個變量,挺重要的,具體做用下面會詳細說明。而且若是須要插入的元素數組的長度小於1,那麼也就沒有必要繼續往下走了,直接return this
進行鏈式操做。
return this.each(function(_, target) { // xxx nodes.forEach(function(node) { // xxx // 注意這行,全部的插入操做都經過insertBefore函數完成 parent.insertBefore(node, target) // xxx }) })
整個後續代碼就是兩層嵌套循環,第一層遍歷當前選中的元素集合,第二層就是須要插入的nodes節點集合。經過兩個循環來最終完成元素的插入操做,而且很重要的一點是,不論是append
仍是after
等方法都是經過insertBefore
來模擬完成的。
肯定parent節點以及target目標節點
經過上面的分析咱們知道經過insertBefore(在當前節點的某個子節點以前再插入一個子節點)來完成節點的插入,很重要的幾個因素就是
parentNode.insertBefore(newNode, referenceNode)
因此肯定以上1和3就顯得極其重要了。怎麼肯定呢?
return this.each(function(_, target) { parent = inside ? target : target.parentNode // convert all methods to a "before" operation target = operatorIndex == 0 ? target.nextSibling : operatorIndex == 1 ? target.firstChild : operatorIndex == 2 ? target : null // xxx })
inside是個啥啊!!!,讓咱們回到頂部看這段
adjacencyOperators = ['after', 'prepend', 'before', 'append'] adjacencyOperators.forEach(function (operator, operatorIndex) { var inside = operatorIndex % 2 // xxx })
因此說當要往$原型上添加的方法是prepend
和append
的時候inside
爲1也就是真,當爲after
和before
的時候爲0也就是假。
由於prepend
和append
都是往當前選中的元素內部添加新節點,因此parent
固然就是target
自己了,可是after
和before
確是要往選中的元素外部添加新節點,天然parent
就變成了當前選中元素的父節點。到這裏上面的三要素1,已經明確了,還有3(target)如何肯定呢?
target = operatorIndex == 0 ? target.nextSibling : operatorIndex == 1 ? target.firstChild : operatorIndex == 2 ? target : null
好啦三要素3頁已經明確了,接下來咱們把重要放在第二個循環。
將新節點插入到指定位置
nodes.forEach(function(node) { if (copyByClone) node = node.cloneNode(true) else if (!parent) return $(node).remove() parent.insertBefore(node, target) // 處理插入script狀況 })
在將節點插入到指定位置的前有一個判斷,若是copyByClone
爲真,就將要插入的新節點複製一份。爲何要這麼作呢?咱們來看個例子。
<ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul>
let $list = document.querySelector('.list') let $listLi = document.querySelectorAll('.list li') let createEle = (tagName, text) => { let ele = document.createElement(tagName) ele.innerHTML = text return ele } let $span1 = createEle('span', 'span1') let $span2 = createEle('span', 'span2') Array.from($listLi).forEach((target) => { [$span1, $span2].forEach((node) => { // node = node.cloneNode(true) $list.insertBefore(node, target) }) })
先將cloneNode那部分給註銷了,咱們指望往三個li的前面都插入兩個span,可是結果會怎麼樣呢?只有最後一個節點前面能夠成功地插入兩個span節點。這樣就不是咱們先要的結果了,根據insertBefore mdn解釋,若是newElement已經在DOM樹中,newElement首先會從DOM樹中移除。,因此當咱們須要往多個li中插入一樣相似的兩個節點的時候,才須要將新節點克隆一份再插入。
咱們接着回到源碼。
nodes.forEach(function(node) { if (copyByClone) node = node.cloneNode(true) else if (!parent) return $(node).remove() parent.insertBefore(node, target) // 處理插入script狀況 })
若是須要(當前選中元素的個數大於1)克隆節點的時候,先將新節點克隆一份,若是沒有找到對應的parent節點,就講要插入的新節點刪除,最後經過insertBefore
方法插入新節點。
到了這裏咱們彷佛已經完成了從
建立新節點
=> 將新節點插入到指定位置
的操做了。任務好像已經完成了,可是革命還沒有成功,同志仍需努力啊。接下來看最後一點代碼,主要是處理,當插入的節點是script
標籤的時候,須要手動去執行其包含的js代碼。
var parentInDocument = $.contains(document.documentElement, parent) if (parentInDocument) traverseNode(node, function(el) { if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' && (!el.type || el.type === 'text/javascript') && !el.src) { var target = el.ownerDocument ? el.ownerDocument.defaultView : window target['eval'].call(target, el.innerHTML) } })
先提早看一下traverseNode這個函數的代碼
function traverseNode(node, fun) { fun(node) for (var i = 0, len = node.childNodes.length; i < len; i++) traverseNode(node.childNodes[i], fun) }
這個函數的主要做用就是將傳入的node節點做爲參數去調用傳入的fun函數。而且遞歸的將node節點的子節點,交給fun去處理。
接下來繼續看。
首先經過$.contains
方法判斷parent
是否在document
文檔中,接着須要知足一下幾個條件纔去執行後續操做。
肯定window對象
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
新節點存在ownerDocument mdn則window對象爲defaultView mdn,不然使用window對象自己。
這裏主要會考慮node節點是iframe種的元素狀況,才須要作三目處理。
最後即是調用target['eval'].call(target, el.innerHTML)
去執行script中的代碼了。
到這裏咱們終於知道了'after', 'prepend', 'before', 'append'實現全過程(偷樂一下?,不容易啊)。
緊接着咱們繼續往前走,前面說了插入操做有不少個方法,其中insertAfter
,insertBefore
,prependTo
,appendTo
的實現基於上述幾個方法。
// append => appendTo // prepend => prependTo // before => insertBefore // after => insertAfter $.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function (html) { $(html)[operator](this) return this }
若是是append
或者prepend
則往$原型上添加appendTo
和prependTo
方法,若是是before
或者after
的時候,便往$的原型上添加insertBefore
和insertAfter
方法。由於其兩兩對應的方法本質上是一樣的功能,只是在使用上有點相反的意思,因此簡單的反向調用一下就能夠了。
獲取或設置對象集合中元素的HTML內容。當沒有給定content參數時,返回對象集合中第一個元素的innerHtml。當給定content參數時,用其替換對象集合中每一個元素的內容。content能夠是append中描述的全部類型 zeptojs_api
例子
1. html() ⇒ string 2. html(content) ⇒ self 3. html(function(index, oldHtml){ ... }) ⇒ self
源碼實現
html: function (html) { return 0 in arguments ? this.each(function (idx) { var originHtml = this.innerHTML $(this).empty().append(funcArg(this, html, idx, originHtml)) }) : (0 in this ? this[0].innerHTML : null) }
當沒有傳html參數的時候,先判斷當前選中的元素是否存在,存在則讀取第一個元素的innerHTML並返回,不然直接返回null
(0 in this ? this[0].innerHTML : null)
當傳了html參數的時候。對當前選中的元素集合進行遍歷設置,先保存當前元素的innerHTML到originHtml變量中,再將當前元素的innerHTML置空,並將funcArg函數執行以後返回的html插入到當前元素中。
function funcArg(context, arg, idx, payload) { return isFunction(arg) ? arg.call(context, idx, payload) : arg }
能夠看到funcArg會對傳入arg進行類型判斷,若是是函數,就把對應的參數傳入函數再將函數的執行結果返回,不是函數就直接返回arg。
獲取或者設置全部對象集合中元素的文本內容。當沒有給定content參數時,返回當前對象集合中第一個元素的文本內容(包含子節點中的文本內容)。當給定content參數時,使用它替換對象集合中全部元素的文本內容。它有待點似 html,與它不一樣的是它不能用來獲取或設置 HTML。zeptojs_api
text: function (text) { return 0 in arguments ? this.each(function (idx) { var newText = funcArg(this, text, idx, this.textContent) this.textContent = newText == null ? '' : '' + newText }) : (0 in this ? this.pluck('textContent').join("") : null) }
text實現方法與html比較相似有些不一樣的是沒有傳參數的時候,html是獲取第一個元素的innerHTMLtext則是將當前全部元素的textContent拼接起來並返回.
經過深度克隆來複制集合中的全部元素。zeptojs_api
clone: function () { return this.map(function () { return this.cloneNode(true) }) }
對當前選中的元素集合進行遍歷操做,底層仍是用的瀏覽器cloneNode,並傳參爲true表示須要進行深度克隆(其實感受這裏是否是將true設置爲可選參數比較好呢,讓使用者決定是深度克隆與否不是更合理?)
須要注意的地方是cloneNode方法不會複製添加到DOM節點中的Javascript屬性,例如事件處理程序等,這個方法只複製特性,子節點,其餘一切都不會複製,IE在此存在一個bug,即他會賦值事件處理程序,因此咱們建議在賦值之間最好先移除事件處理程序(摘自《JavaScript高級程序設計第三版》10.1.1 Node類型小字部分)
用給定的內容替換全部匹配的元素。(包含元素自己) zeptojs_api
replaceWith: function(newContent) { return this.before(newContent).remove() }
源碼實現其實很簡單分兩步,第一步調用前面咱們講的before方法將制定newContent插入到元素的前面,第二部步將當前選中的元素刪除。天然也就達到了替換的目的。
在全部匹配元素外面包一個單獨的結構。結構能夠是單個元素或 幾個嵌套的元素zeptojs_api/#wrapAll
wrapAll: function (structure) { // 若是選中的元素存在 if (this[0]) { // 則將制定structure結構經過before方法,插入到選中的第一個元素的前面 $(this[0]).before(structure = $(structure)) var children // drill down to the inmost element // 獲取structure的最深層次的第一個子元素 while ((children = structure.children()).length) structure = children.first() // 將當前的元素集合經過append方法添加到structure末尾 $(structure).append(this) } // 反則直接返回this進行後續的鏈式操做 return this }
源碼實現直接看註釋就能夠了,這裏須要注意一下children
函數是獲取對象集合中全部的直接子節點。而first
函數則是獲取當前集合的第一個元素。
另外咱們看一下下面兩個例子。
<ul class="box"> <li>1</li> <li>2</li> </ul> <div class="wrap"> </div> <div class="wrap"> </div>
$('.box').wrapAll('.wrap')
執行上述代碼以後dom結構會變成
<div class="wrap"> <ul class="box"> <li>1</li> <li>2</li> </ul> </div> <div class="wrap"> <ul class="box"> <li>1</li> <li>2</li> </ul> </div> <ul class="box"> <li>1</li> <li>2</li> </ul>
能夠看到原來ul結構仍是存在,彷彿是複製了一份ul及其子節點到wrap中被包裹起來。
接下來再看一個例子,惟一的區別就在wrap結構中嵌套了基層。
<ul class="box"> <li>1</li> <li>2</li> </ul> <div class="wrap"> <div class="here"></div> <div></div> </div> <div class="wrap"> <div class="here"></div> <div></div> </div>
可是最後執行$('.box').wrapAll('.wrap')
獲得的dom結果是。
<div class="wrap"> <div class="here"> <ul class="box"> <li>1</li> <li>2</li> </ul> </div> <div></div> </div> <div class="wrap"> <div class="here"></div> <div></div> </div>
嘿嘿能夠看到,ul原來的結構不見了,被移動到了第一個wrap的第一個子節點here中。具體緣由是什麼呢?你們能夠從新回去看一下append的核心實現。
在每一個匹配的元素外層包上一個html元素。structure參數能夠是一個單獨的元素或者一些嵌套的元素。也能夠是一個html字符串片斷或者dom節點。還能夠是一個生成用來包元素的回調函數,這個函數返回前兩種類型的包裹片斷。zeptojs_api/#wrapAll
wrap: function (structure) { var func = isFunction(structure) // 當前選中的元素不爲空,而且structure不是一個函數 if (this[0] && !func) // 就將structure轉化後的第一個元素賦值給dom元素 var dom = $(structure).get(0), // 若是dom元素的parentNode存在或者當前選中的元素個數大於1那麼clone爲true clone = dom.parentNode || this.length > 1 // 對當前選中元素進行遍歷而且調用wrapAll方法 return this.each(function (index) { $(this).wrapAll( // 若是structure爲函數,則將當前的元素和對應的索引傳入函數 func ? structure.call(this, index) : // 若是clone爲true,則使用拷貝的副本 clone ? dom.cloneNode(true) : dom ) }) }
將每一個元素中的內容包裹在一個單獨的結構中 zeptojs_api/#wrapInner
wrapInner: function (structure) { // 判斷structure是否爲函數 var func = isFunction(structure) // 對當前元素集合進行遍歷處理 return this.each(function (index) { // contents => 獲取當前元素的全部子節點(包括元素節點和文本節點) var self = $(this), contents = self.contents(), // structure爲函數則將其執行結果賦值爲dom,不然直接將其賦值 dom = func ? structure.call(this, index) : structure // 當前元素的子節點不爲空,則調用wrapAll,不然直接將dom插入self當前元素便可 contents.length ? contents.wrapAll(dom) : self.append(dom) }) }
須要注意的是這個函數和前面的wrapAll和wrap有點不同,這裏強調的是將當前元素中的內容(包括元素節點和文本節點)進行包裹。
移除集合中每一個元素的直接父節點,並把他們的子元素保留在原來的位置
unwrap: function () { // 經過parent()獲取當前元素集合的全部直接父節點 // 將獲取到的父節點集合進行遍歷 this.parent().each(function () { // 將該父節點替換爲該父節點的全部子節點 $(this).replaceWith($(this).children()) }) return this },
呼呼呼,終於寫完了,快累死了。歡迎你們指正文中的問題。
《JavaScript高級程序設計第三版》
文章記錄
form模塊
zepto模塊
event模塊
ajax模塊