跟著Zepto學dom操作(一)

前端一小卒發表於2017-11-04

之前有嘗試著去閱讀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獲取到的內容如下顯示:

svg.png
svg.png

通過讀取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)
  })
}複製程式碼

相關文章