之前有嘗試著去閱讀jQuery原始碼,但是由於原始碼過長再加上自己技術上有點不到家,在嘗試過幾遍之後不得不遺憾的選擇放棄。對dom的操作一直都使用jQuery,如果讓我用原生的JS去操作dom,會發現自己不能很快的實現需求,所以這次選擇Zepto的原始碼去深入挖掘dom操作。注意,此次選用的Zepto選用最新1.2.0的版本,所以不太適用於PC瀏覽器。
selector選擇器
jQuery有一個非常強大的DOM選擇器引擎Sizzle,選擇速度堪稱業內頂尖,Zepto號稱移動端的jQuery,那麼其肯定需要一個自己的選擇器,現在我們來看看這個非常小巧輕便的選擇器。
// 該正則匹配a-z,A-Z,下劃線,-所連著的單詞,驗證是否為單個類名
// 'app-532'為true 'app app123'為false
var simpleSelectorRE = /^[\w-]*$/;
zepto.qsa = function(element, selector){
//找到的元素
var found,
//開頭元素為ID
maybeID = selector[0] == '#',
//開頭元素為class
maybeClass = !maybeID && selector[0] == '.',
//將開頭元素為ID或class的#或.去掉
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector,
//是否為單個選擇還是層級選擇,注意這裡有一個bug(Zepto的bug)。
isSimple = simpleSelectorRE.test(nameOnly)
return (element.getElementById && isSimple && maybeID) ?
( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
//當element不為元素節點,文件節點和文件片段節點時返回為[]
(element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
[].slice.call(
//為單個選擇且不為ID同時支援getElementsByClassName時,當為class則使用getElementsByClassName,不為class,則使用element.getElementsByTagName
//否則為querySelectorAll
isSimple && !maybeID && element.getElementsByClassName ?
maybeClass ? element.getElementsByClassName(nameOnly) :
element.getElementsByTagName(selector) :
element.querySelectorAll(selector)
)
}
dom = zepto.qsa(document, selector)
//querySelectorAll方法支援IE8+,不過在IE8中只支援css2.1選擇器。複製程式碼
Zepto的選擇器非常的小巧,但是又帶來了一些問題,當一個元素其id為'app.item'時,使用$('#app.item')往往會出現選擇不上的情況,同時Zepto選用querySelectorAll來進行層級選擇,而querySelectorAll自身的效能缺陷會導致Zepto選擇器的效能過慢,具體可去選擇器API沒有效能優化,慎用詳細瞭解該效能問題。
解決了選擇元素的問題,我們現在可以來看看具體的dom操作了。
attr
attr:function(name,value){
var result;
//1 in arguments的作用就是判斷其是否傳入了value
//只傳入name且為string的時候則為讀取匹配到的第一個元素name屬性
return (typeof name == 'string' && !(1 in arguments)) ?
(0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) :
this.each(function(idx){
if (this.nodeType !== 1) return
//如果為name為物件的話,則可以進一步的迴圈name
if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
//由於value為非函式,所以funcArg直接返回的是value
else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
})
}
function funcArg(context, arg, idx, payload) {
return isFunction(arg) ? arg.call(context, idx, payload) : arg
}
function setAttribute(node,name,value){
value == null ? node.removeAttribute(name) : node.setAttribute(node,value)
}複製程式碼
class
var classCache = {};
//classCache中其初始值為{},當呼叫該函式時,會返回一個正規表示式,其匹配開頭或者空白(包括空格、換行、tab縮排等)+name+結尾或者空白(包括空格、換行、tab縮排等)。不過還是沒有弄懂為什麼要將class的正則儲存起來。
function classRE(name) {
return name in classCache ?
classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
}
//去獲取className
function className(node, value){
var klass = node.className || '',
svg = klass && klass.baseVal !== undefined;
//讀取
if (value === undefined) return svg ? klass.baseVal : klass
//設定
svg ? (klass.baseVal = value) : (node.className = value)
}
//判斷是否有該class
hasClass: function(name){
if (!name) return false
//當this中只要有一個元素包含name這個class,即返回true
return [].some.call(this, function(el){
return this.test(className(el))
}, classRE(name))
},
//新增元素
addClass: function(name){
if (!name) return this
return this.each(function(idx){
//排除非dom元素
if (!('className' in this)) return
classList = []
var cls = className(this), newName = funcArg(this, name, idx, cls)
//對當前元素的className進行遍歷,如果沒有改元素,則將元素push進classList。
newName.split(/\s+/g).forEach(function(klass){
if (!$(this).hasClass(klass)) classList.push(klass)
}, this)
classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
})
},
//移除元素
removeClass: function(name){
return this.each(function(idx){
if (!('className' in this)) return
//當name為空,則移除所有的class
if (name === undefined) return className(this, '')
classList = className(this)
funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){
classList = classList.replace(classRE(klass), " ")
})
className(this, classList.trim())
})
},
toggleClass: function(name, when){
if (!name) return this
return this.each(function(idx){
var $this = $(this), names = funcArg(this, name, idx, className(this))
names.split(/\s+/g).forEach(function(klass){
//存在則刪除,不存在則新增
(when === undefined ? !$this.hasClass(klass) : when) ?
$this.addClass(klass) : $this.removeClass(klass)
})
})
},複製程式碼
在函式className中有個判斷svg的特殊方法,即svg= klass && klass.baseVal !== undefined
。svg這個元素比較特殊,其通過document.getElementsByClassName('Icon--logo')[0].className
獲取到的內容如下顯示:
通過讀取className.baseVal的形式來判斷是否為svg元素。同時設定其class的方式也不能通過className的形式直接設定,也要通過className.baseVal的形式去設定才能生效。
Zepto的class設定方式非常的巧妙,相容性也比較好,但是如果只是在移動端使用的話,完全可以用HTML5中提供的classList
介面去取代,該介面的相容性非常的棒,目前(2017年11月)能夠直接使用,無需考慮相容性問題,是的,svg元素也能夠使用。下面將用classList的形式改寫下這些函式(注:下面的函式只能設定讀取一個class,如果要實現多個class,稍微在這基礎上改寫下就行了):
addClass:function(name){
if(!name) return this
return this.forEach(funciton(item){
//classList的add方法:如果這些類已經存在於元素的屬性中,那麼它們將被忽略
item.classList.add(name)
})
}
removeClass:function(name){
return this.forEach(funciton(item){
if(!name){
var klassList = item.className,
svg = klassList && klassList.baseVal !== undefined;
svg ? klassList.baseVal = '' : klassList = ''
}
item.classList.remove(name)
})
}
toggleClass:function(name){
return this.forEach(function(item){
item.classList.toggle(name)
})
}複製程式碼