迷你版jQuery——zepto核心原始碼分析

葉小釵發表於2014-07-25

前言

zepto號稱迷你版jQuery,並且成為移動端dom操作庫的首選
事實上zepto很多時候只是借用了jQuery的名氣,保持了與其基本一致的API,其內部實現早已面目全非!
艾倫分析了jQuery,小釵暫時沒有那個本事分析jQuery,這裡就恬不知恥說說自己對zepto的原始碼理解,希望對各位有用
首先zepto的出現其實還是很討巧的,他看見了巨人jQuery在移動浪潮來臨時的轉身慢、牽掛多的問題
馬上搞出了一套輕量級類jQuery框架程式碼,核心程式碼1000行不到,快速佔領了移動端的市場,所以天下武學無堅不摧,為快不破啊!!!
也如艾倫所言,jQuery狹義的講其實就是dom操作庫
zepto將這點發揚光大,並且拋棄了瀏覽器相容的包袱,甚至CSS3的字首都不給加,這些因素造就了zepto小的事實,於是我們開始學習他吧
此文只是個人對zepto的粗淺理解,有誤請提出

核心組成

zepto現在也採用了模組拆分,這樣讀起來其實程式碼十分清晰,門檻也低了很多,整個zepto核心模組保持在900行以內
我們說他很好的發揚了dom庫特點便是因為這900行基本在幹dom操作的活
核心模組有以下部分組成:

① 閉包變數、工具類方法定義

這個部分主要為後面服務,比如說什麼isFunction/isPlainObject/children
其中有一個比較特別的變數是
zepto = {};

這個變數貫穿始終,也是zepto與jQuery很不一樣的地方,jQuery是一個類,會建立一個個例項,而zepto本身就只是一個物件......

② zepto與jQuery的$

zepto第二階段乾的事情便是定義了一個類
$ = function(selector, context){
  return zepto.init(selector, context)
}

而我們開始便說了zepto只是一個物件,而zepto.init也僅僅是返回了一個類陣列的東西,於是我們這裡便看到了zepto與jQuery的驚人差異

第一觀感是zepto沒有類操作!我們使用$('')的操作返回的也是zepto的例項
$對於zepto來說僅僅是一個方法,zepto卻使用了非正規手法返回了例項......
從這裡看整個zepto其實和jQuery就差距大了,zepto的$方法返回了一個Object的例項,而jQuery的$返回的是真資格的jQuery物件
而從後面看其實zepto也是返回的一個例項但是與jQuery的實現有所不同,那麼zepto是怎麼實現例項返回的呢?

③ zepto與jQuery的$.fn

我們知道jQuery的$.fn指向的是jQuery.prototype的原型物件,而zepto的fn就是一個簡單物件
$.fn = {};
zepto的第三部分便是擴充套件$函式,我們使用的$的方法事實上都是其靜態方法,與原型鏈一毛錢關係都沒有
以上便是zepto核心模組的實現,很乾淨的實現,僅僅是dom操作,不涉及事件或者Ajax操作,簡單來說zepto的實現是這個樣子的
 1 var zepto = {}, $;
 2      
 3 zepto.init = function (selector, context) {
 4   var domArr = [];
 5   //這個__proto__是系統級變數,我覺得zepto不該重置 ,但是不重置的話例項便找不到方法了!!!
 6   domArr.__proto__ = $.fn
 7   domArr.selector = selector;
 8   //一些列操作
 9   return domArr;
10 };
11 
12 $ = function (selector, context) {
13   return zepto.init(selector, context);
14 };
15 
16 $.fn = {
17   addClass: function () { },
18   attr: function () { }
19 };

這裡有段非常關鍵的程式碼是:

domArr.__proto__ = $.fn;
如果是沒有這段程式碼的話, domArr便是屬於array的例項,便不能使用$.fn中的方法了,但是他這裡重置了__proto__的指向所以就能用了
PS:由於IE是不認這個屬性的,所以IE必定會報錯
由於這裡的改下,本來domArr也會有一些變化:
 1 dom.__proto__.constructor
 2 function Array() { [native code] }
 3 
 4 dom.__proto__.constructor
 5 function Object() { [native code] }
 6 
 7 zepto.Z = function(dom, selector) {
 8   dom = dom || []
 9   dom.__proto__ = $.fn
10   dom.selector = selector || ''
11   return dom
12 }
13 //最後加上一句:
14 zepto.Z.prototype = $.fn
如此一來,我們所有的$方法返回的東西其實就變成了zepto.Z的例項了,這裡的實現原理其實也有點繞口:
建構函式zepto.Z 包含一個原型 $.fn(zepto.Z的prototype被重寫了)
原型$.fn具有一個Constructor回值建構函式zepto.Z(這裡由於其粗暴的幹法其實直接指向了Object,這裡關係其實已經丟失)
比較不正經的是居然是通過重寫__proto__實現,感覺怪怪的,好了核心模組介紹結束,我們便進入入口函式的解析了

分解$方法

$是zepto的入口,具有兩個引數selector選擇器與context選擇範圍,這裡看著是兩個引數,事實上各個引數不同會造成不同的實現
$方法相當於一個黑盒子,使用者會根據自己的想法獲得自己想要的結果,這也會導致$的實現變得複雜:
 1 $('div');
 2 //=> all DIV elements on the page
 3 $('#foo');
 4 //=> element with ID "foo"
 5 
 6 // create element:
 7 $("<p>Hello</p>");
 8 //=> the new P element
 9 // create element with attributes:
10 $("<p />", {
11   text: "Hello",
12   id: "greeting",
13   css: { color: 'darkblue' }
14 });
15 //=> <p id=greeting style="color:darkblue">Hello</p>
16 
17 // execute callback when the page is ready:
18 $(function ($) {
19   alert('Ready to Zepto!')
20 });

我們現在來分析其每一種實現

選擇器

zepto主要乾的事情還是做dom選擇,這裡包括標籤選擇、id選擇、類選擇等,少了sizzle的複雜,直接使用了querySelectorAll的實現真的很偷懶
PS:同一個頁面出現相關相同id的話querySelectorAll會出BUG,這個大家要小心處理!!!
這裡篩選的流程是:
① 執行$(selector)方法
② 執行zepto.init(selector)方法,init裡面的邏輯就有點小複雜了
判斷selector是不是一個字串,這裡需要是乾淨的字串,並且context為undefined(這裡差距不大,了不起是查詢範圍的問題)
③ 經過上述邏輯處理,高高興興進入zepto.qsa(document, selector)邏輯
這裡的邏輯比較簡單直接呼叫判斷下選擇器的型別(id/class/標籤)就直接使用對應的方法獲取元素即可
zepto.qsa = function(element, selector){
  var found,
      maybeID = selector[0] == '#',
      maybeClass = !maybeID && selector[0] == '.',
      nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
      isSimple = simpleSelectorRE.test(nameOnly)
  return (isDocument(element) && isSimple && maybeID) ?
    ( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
    (element.nodeType !== 1 && element.nodeType !== 9) ? [] :
    slice.call(
      isSimple && !maybeID ?
        maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
        element.getElementsByTagName(selector) : // Or a tag
        element.querySelectorAll(selector) // Or it's not simple, and we need to query all
    )
}
View Code

建立元素

$方法的第二大功能便是建立元素了,比如我們這裡的
$("<p>Hello</p>");

這裡依舊會經過zepto.init的處理,判斷是否具有尖括號(<),有的話便會進入神奇的fragment邏輯建立文件碎片

dom = zepto.fragment(selector, RegExp.$1, context)
這裡有一個正規表示式對傳入的html進行解析,目標是標籤名
PS:zepto對p標籤的解析也會出問題,不建議使用
zepto.fragment = function(html, name, properties) {}

到fragment方法時,會傳入html和那麼並且會有相關屬性,但是我們一般不這樣幹,僅僅希望建立DOM

zepto.fragment = function(html, name, properties) {
  var dom, nodes, container

  // A special case optimization for a single tag
  if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))

  if (!dom) {
    if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
    if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
    if (!(name in containers)) name = '*'

    container = containers[name]
    container.innerHTML = '' + html
    dom = $.each(slice.call(container.childNodes), function(){
      container.removeChild(this)
    })
  }

  if (isPlainObject(properties)) {
    nodes = $(dom)
    $.each(properties, function(key, value) {
      if (methodAttributes.indexOf(key) > -1) nodes[key](value)
      else nodes.attr(key, value)
    })
  }

  return dom
}
View Code
裡面的邏輯各位自己去看,我這裡不多說了,還是很簡單的,大概的想法是
建立一個空的div元素,將字串裝載,然後遍歷div的子元素,最後返回一個node的集合陣列,這個也就是我們實際需要的......
這個樣子,建立標籤或者selector選擇器得到的結果是一致的
其它邏輯大同小異,我們直接就過了,zepto核心入口邏輯就到此結束了......

fn的實現

fn中包含了zepto的很多功能,要一一說明就多了去了,首先由$擴充套件開始說
除了原型擴充套件外還為$包含了很多靜態方法,比如什麼uuid,isFunction,然後就開始了原型鏈擴充套件之路
$.fn與zepto.Z.prototype指向的是同一空間,這裡達到了是擴充套件原型鏈的效果
這裡抽2個常用API來看看,比如這裡的attr
attr: function(name, value){
  var result
  return (typeof name == 'string' && value === undefined) ?
    (this.length == 0 || this[0].nodeType !== 1 ? undefined :
      (name == 'value' && this[0].nodeName == 'INPUT') ? this.val() :
      (!(result = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : result
    ) :
    this.each(function(idx){
      if (this.nodeType !== 1) return
      if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
      else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
    })
},
function setAttribute(node, name, value) {
  value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
}
我們看到他這裡直接將其轉換為了元素DOM操作,沒有什麼好說的,只是如果value不為undefined時,裡面有一個迴圈為屬性賦值的動作
再看這裡的html介面
html: function(html){
  return arguments.length === 0 ?
    (this.length > 0 ? this[0].innerHTML : null) :
    this.each(function(idx){
      var originHtml = this.innerHTML
      $(this).empty().append( funcArg(this, html, idx, originHtml) )
    })
},
function funcArg(context, arg, idx, payload) {
  return isFunction(arg) ? arg.call(context, idx, payload) : arg
}
這裡其實是先將this清空,然後裝載新的dom結構,這裡與設定innerHTML有所不同,append會執行其中的js,設定innerHTML不會執行
剩下的介面有興趣的朋友自己去看吧,zepto這裡實現還是比較簡單的。
這裡值得一說的是,一些API你直接去看可能看不懂,這個時候就動手寫寫,實現相同的功能,然後對程式碼進行重構,最後重構下來程式碼和他寫的就差不多了,這裡並不是程式碼難,而是他那種寫法不太好看。

事件實現

一個稍微成熟點的框架或者說稍微成熟點的團隊,一般會對原生的一些東西進行封裝,原因是他們可能需要擴充套件非常典型的例子便是事件與settimeout
以setTimeout為例,在webapp中每次view的切換應該清理所有的settimeout,但是我們知道clearTimeout()是必須傳入id的,所以我們不能這麼幹
現在回到javascript事件這塊,最初事件的出現可能僅僅是為了做瀏覽器相容
那麼現在我們依舊會使用zepto提供的事件主要原因就是其擴充套件的一些功能,比如委託與名稱空間等,最重要的還是事件控制程式碼移除
javascript事件的移除很是嚴苛,要求必須與之一致的引數,比如:
el.addEventListerner(type, fn, capture);
el.removeEventListerner(type, fn, capture);

兩者引數需要完全一致,而我們的fn很多時候就是個匿名函式甚至是物件,很多時候定義後控制程式碼引用就丟了,我們根本沒法將其保持一致

這個時候這個控制程式碼便無法釋放,所以我們需要對事件進行封裝,我們這裡便進入zepto event的實現,學習這個還是看入口點

事件註冊

簡單來說使用zepto繫結事件一般是這樣:

① $.on(type, fn)
② $.bind(type, fn)
③ $.click(fn)
④ ......
事實上,這些方式差距不大,特別是第二種只是做了一個語法糖,比如:
$.click = function (fn) {
    return this.bind('click', fn);
}

事實上他還是呼叫的$.bind實現事件繫結,換個思維方式,其實整個zepto事件實現可以濃縮成這麼幾句話:

var eventSet = {
    el: {fnType: []}
};
function on(type, fn) {}
function off(type, fn) {}
這個便是zepto事件核心程式碼......當然這裡還差了一個trigger,這裡便是與傳統自建系統不一樣的地方,他的觸發是通過瀏覽器處理
這個是一個標準的釋出訂閱系統,我們對瀏覽器的操作會生產事件,這個時候瀏覽器會根據我們的行為通知對應的事件接收者處理事件
所有的繫結最終呼叫的皆是$.on,而on或者off的最終歸宿為區域性閉包add和remove方法
$.fn.on = function(event, selector, data, callback, one){
  var autoRemove, delegator, $this = this
  if (event && !isString(event)) {
    $.each(event, function(type, fn){
      $this.on(type, selector, data, fn, one)
    })
    return $this
  }

  if (!isString(selector) && !isFunction(callback) && callback !== false)
    callback = data, data = selector, selector = undefined
  if (isFunction(data) || data === false)
    callback = data, data = undefined

  if (callback === false) callback = returnFalse

  return $this.each(function(_, element){
    if (one) autoRemove = function(e){
      remove(element, e.type, callback)
      return callback.apply(this, arguments)
    }

    if (selector) delegator = function(e){
      var evt, match = $(e.target).closest(selector, element).get(0)
      if (match && match !== element) {
        evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
        return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
      }
    }

    add(element, event, callback, data, selector, delegator || autoRemove)
  })
}
View Code

這裡的event可以是以空格分隔的字串,一般情況下是單一的事件

event => 'mousedown touchstart'
event => 'click'
然後這裡開始了處理邏輯:
① 引數處理
第一步當然是做引數處理,會修正引數,比如你沒有傳事件控制程式碼,這裡會給個預設的,然後開始迴圈繫結,因為我們使用$()返回的是一個陣列
進入迴圈邏輯後,this與element便是真資格的dom元素了,未經雕琢,開始是對one的處理,我們不予關注,繼續向下便進入第一個關鍵點
簡單情況下我們的selector為undefined,所以這裡錯過了一個事件委託的重要邏輯,我們先不予理睬,再往下便進入了閉包方法add了
這個情況下selector與delegator為undefined,僅僅是前3個引數有效

add在event事件中扮演了重要的角色

function add(element, events, fn, data, selector, delegator, capture){
  var id = zid(element), set = (handlers[id] || (handlers[id] = []))
  events.split(/\s/).forEach(function(event){
    if (event == 'ready') return $(document).ready(fn)
    var handler = parse(event)
    handler.fn = fn
    handler.sel = selector
    // emulate mouseenter, mouseleave
    if (handler.e in hover) fn = function(e){
      var related = e.relatedTarget
      if (!related || (related !== this && !$.contains(this, related)))
        return handler.fn.apply(this, arguments)
    }
    handler.del = delegator
    var callback = delegator || fn
    handler.proxy = function(e){
      e = compatible(e)
      if (e.isImmediatePropagationStopped()) return
      e.data = data
      var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
      if (result === false) e.preventDefault(), e.stopPropagation()
      return result
    }
    handler.i = set.length
    set.push(handler)
    if ('addEventListener' in element)
      element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
  })
}
View Code

第一段程式碼就很重要:

var id = zid(element)
function zid(element) {
  return element._zid || (element._zid = _zid++)
}

這裡的zid非常關鍵,這裡的element為與原生物件,這裡在上面加了一個_zid的屬性,這個屬性會跟隨其由始至終,不會丟失,如果是zepto封裝的dom物件的話,就很容易丟失,因為每次根據$()建立的dom都是新的,這個_zid放到原生屬性上是很有意義的

第二個變數也很關鍵:
set = (handlers[id] || (handlers[id] = []))

我們所有繫結的事件以_zid為鍵值放在了外部閉包環境handlers物件中,每一個id對應的為一個陣列,這個與繫結先後順序相關

然後進入具體繫結邏輯:
完了這裡會考慮是'mousedwon touchstart'的情況所以會有一個迴圈,我們這裡由於只是click便不予理睬了,ready事件我們也直接忽略,進入邏輯後關鍵點來了
這裡定義了一個handler物件,這個物件會存於handlers裡面
var handler = parse(event)
handler.fn = fn
handler.sel = selector

function parse(event) {
  var parts = ('' + event).split('.')
  return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
}
這裡會解析event引數,取出其中的名稱空間,比如:'click.ui'或者'click.namespace'
返回的物件,第一個是真正繫結的事件Type,第二個是其名稱空間:
handler = {
  e: 'click',
  ns: ''//我這裡為null  
}
後面再為handler物件擴充套件fn與selector屬性,這裡的fn尤其關鍵!!!
我們知道,繫結時若是使用的是匿名函式的話,其引用會丟失,但是這裡就把他保持下來存到了handlers中,為後面off消除控制程式碼提供了條件
下面會有段程式碼,處理mouse事件,用以模擬mouseenter, mouseleave,我們簡單來看看其實現:
// emulate mouseenter, mouseleave
if (handler.e in hover) fn = function(e){
  var related = e.relatedTarget
  if (!related || (related !== this && !$.contains(this, related)))
    return handler.fn.apply(this, arguments)
}
$.contains = function(parent, node) {
  return parent !== node && parent.contains(node)
}

relatedTarget 事件屬性返回與事件的目標節點相關的節點。
對於 mouseover 事件來說,該屬性是滑鼠指標移到目標節點上時所離開的那個節點。
對於 mouseout 事件來說,該屬性是離開目標時,滑鼠指標進入的節點。
對於其他型別的事件來說,這個屬性沒有用。
所以我們使用mouseenter,其實mousemove依舊一直在執行,只不過滿足要求才會進入mouseleave繫結的回撥

這裡結束便進入事件繫結的真正邏輯,這裡又為handler新增了一個proxy屬性,將真實的事件回撥封裝了,封裝的主要原因是做事件代理,事件代理一塊我們先不關注
我們看到proxy將我們的回撥fn(已經變成了callback),做一次封裝,直接為element註冊事件了,其影響會在觸發時產生:
function compatible(event, source) {
  if (source || !event.isDefaultPrevented) {
    source || (source = event)

    $.each(eventMethods, function(name, predicate) {
      var sourceMethod = source[name]
      event[name] = function(){
        this[predicate] = returnTrue
        return sourceMethod && sourceMethod.apply(source, arguments)
      }
      event[predicate] = returnFalse
    })

    if (source.defaultPrevented !== undefined ? source.defaultPrevented :
        'returnValue' in source ? source.returnValue === false :
        source.getPreventDefault && source.getPreventDefault())
      event.isDefaultPrevented = returnTrue
  }
  return event
}
View Code

觸發事件時他這裡首先會對事件引數event做一次封裝返回,首先將三大事件物件進行新增介面

這裡重置的一個原因是處理stopImmediatePropagation不支援的瀏覽器
然後會執行真正的回撥,這裡會傳入相關引數,並將作用域指向element,於是事件註冊到事件定義第一階段結束
不一樣的是事件委託,比如:
el1.on('click', '#Div1', function (e) {
  s = '';
});

具有selector引數後在add處便會處理不一致,會多出一段邏輯將真正的回撥重置了

if (selector) delegator = function(e){
  var evt, match = $(e.target).closest(selector, element).get(0)
  if (match && match !== element) {
    evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
    return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
  }
}

這段程式碼也很經典,他的影響依舊發生在執行的時候(這裡在add中依舊會被再次處理),首先這裡比較關鍵的程式碼是

match = $(e.target).closest(selector, element).get(0)
這個會根據當前點選最深節點與selector選擇器選擇離他最近的parent節點,然後判斷是否找到,這裡條件還必須滿足找到的不是當前元素
如果找到了,會對event引數做一次處理,為其重寫currentTarget屬性,讓他指向與selector相關的節點(這點很關鍵)
function createProxy(event) {
  var key, proxy = { originalEvent: event }
  for (key in event)
    if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

  return compatible(proxy, event)
}
這裡可以看到,我們如果為document下面的三個元素繫結事件代理,每次點選幾次便會執行幾次事件,只不過會判斷是否進入處理邏輯而已
這裡舉個div與span的例子,如果父級div(wrapper)下面分別為div和span繫結事件的話
$('#wrapper').on('click', '#span', fn);
$('#wrapper').on('click', '#div', fn);
這個事實上會為為wrapper繫結兩個click事件,我們每次點選wrapper區域都會執行兩次click事件,但是是否執行span或者div的事件,要看這裡是否點選到了其子節點(e.target)
這裡處理結束後會進入add方法,與剛剛的邏輯一致,我們便不予理睬了,只是事件代理的情況下event引數連續被compatible了,而原始的事件控制程式碼也被包裹了兩層

事件移除

事件繫結說完,事件移除便比較簡單了,入口是off,統一處理存於閉包remove方法中
$.fn.off = function(event, selector, callback){
  var $this = this
  if (event && !isString(event)) {
    $.each(event, function(type, fn){
      $this.off(type, selector, fn)
    })
    return $this
  }

  if (!isString(selector) && !isFunction(callback) && callback !== false)
    callback = selector, selector = undefined

  if (callback === false) callback = returnFalse

  return $this.each(function(){
    remove(this, event, callback, selector)
  })
}
View Code

程式碼比較簡單,可以直接進入remove的邏輯

這裡有一點值得注意的是,這裡的this指向的是原生dom,並且大家注意到裡面的_zid,callback或者selector我們一般不使用
function remove(element, events, fn, selector, capture){
  var id = zid(element)
  ;(events || '').split(/\s/).forEach(function(event){
    findHandlers(element, event, fn, selector).forEach(function(handler){
      delete handlers[id][handler.i]
    if ('removeEventListener' in element)
      element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  })
}

事件註冊邏輯複雜,刪除卻只需要幾行,在remove時,這裡會根據元素的_zid然後呼叫findHandlers取出存於閉包handlers裡面的事件物件

 1 function findHandlers(element, event, fn, selector) {
 2   event = parse(event)
 3   if (event.ns) var matcher = matcherFor(event.ns)
 4   return (handlers[zid(element)] || []).filter(function(handler) {
 5     return handler
 6       && (!event.e || handler.e == event.e)
 7       && (!event.ns || matcher.test(handler.ns))
 8       && (!fn || zid(handler.fn) === zid(fn))
 9       && (!selector || handler.sel == selector)
10   })
11 }

這裡有個非常巧妙的地方是我們可以根據之前的namespace取出我們註冊的事件集合,比如:

findHandlers處理結束便進入最後的的控制程式碼移除操作即可
而這裡能移除控制程式碼的關鍵又是在於之前將事件控制程式碼handler.proxy儲存下來的原因,至此整個event邏輯結束,值得注意的是element的_zid標識還在,
至於trigger簡單來說便是建立一個event事件物件然後dispatch,僅此而已

手勢處理

zepto提供了一個touch庫進行手勢事件的補充,不得不說其中一個實現很有問題,會造成一些莫名其妙的BUG,但只是以程式碼實現來說還是很清晰的
zepto的touch庫程式碼約150行,其實現方案是:
在載入zepto後為document繫結touchstart、touchmove、touchend事件,根據手指x、y值的位置判斷方向從而觸發tap、doubleTap、swipeLeft等事件,這裡有幾個令人不爽的地方:
① 一旦引入該庫便在全域性繫結事件,每次點選皆會觸發無意義的tap事件
② 若是有人2B的重複引入了zepto事件,那麼tap型別事件會觸發兩次,這個會產生BUG
③ zepto為了實現doubleTap等功能,2B的在touchend時候設定了一個settimeout,然後整個世界都充滿翔了
由於setTimeout的丟擲主幹流程,導致其event引數失效,這個時候就算在tap中執行e.preventDefault()或者什麼都是無效的,這個是導致zepto tap“點透”的罪魁禍首
所以我們若是僅僅為了某塊區域的手勢功能,完全沒有必要引入zepto庫,得不償失的,我們可以以下面程式碼簡單替換,再複雜的功能就沒法了:
(function () {

    //偏移步長
    var step = 20;

    var touch = {};
    var down = 'touchstart';
    var move = 'touchmove';
    var up = 'touchend';
    if (!('ontouchstart' in window)) {
      down = 'mousedown';
      move = 'mousemove';
      up = 'mouseup';
    }

    //簡單借鑑ccd思維做簡要處理
    function swipeDirection(x1, x2, y1, y2, sensibility) {

      //x移動的步長
      var _x = Math.abs(x1 - x2);
      //y移動步長
      var _y = Math.abs(y1 - y2);
      var dir = _x >= _y ? (x1 - x2 > 0 ? 'left' : 'right') : (y1 - y2 > 0 ? 'up' : 'down');

      //設定靈敏度限制
      if (sensibility) {
        if (dir == 'left' || dir == 'right') {
          if ((_y / _x) > sensibility) dir = '';
        } else if (dir == 'up' || dir == 'down') {
          if ((_x / _y) > sensibility) dir = '';
        }
      }
      return dir;
    }

    //sensibility設定靈敏度,值為0-1
    function flip(el, dir, fn, noDefault, sensibility) {
      if (!el) return;

      el.on(down, function (e) {
        var pos = (e.touches && e.touches[0]) || e;
        touch.x1 = pos.pageX;
        touch.y1 = pos.pageY;

      }).on(move, function (e) {
        var pos = (e.touches && e.touches[0]) || e;
        touch.x2 = pos.pageX;
        touch.y2 = pos.pageY;

        //如果view過長滑不動是有問題的
        if (!noDefault) { e.preventDefault(); }
      }).on(up, function (e) {


        if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > step) ||
        (touch.y2 && Math.abs(touch.y1 - touch.y2) > step)) {
          var _dir = swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2, sensibility);
          if (dir === _dir) {
            typeof fn == 'function' && fn();
          }
        } else {
          //tap的情況
          if (dir === 'tap') {
            typeof fn == 'function' && fn();
          }
        }
      });
    }

    function flipDestroy(el) {
      if (!el) return;
      el.off(down).off(move).off(up);
    }

    _.flip = flip;
    _.flipDestroy = flipDestroy;

})();
View Code

其它

累了,略......

Ajax

animate

結語

我們今天對zepto做了一個整理性學習,希望對各位有幫助,最後微博求粉!!!

相關文章