前言
核心組成
① 閉包變數、工具類方法定義
zepto = {};
這個變數貫穿始終,也是zepto與jQuery很不一樣的地方,jQuery是一個類,會建立一個個例項,而zepto本身就只是一個物件......
② zepto與jQuery的$
$ = function(selector, context){ return zepto.init(selector, context) }
而我們開始便說了zepto只是一個物件,而zepto.init也僅僅是返回了一個類陣列的東西,於是我們這裡便看到了zepto與jQuery的驚人差異
③ zepto與jQuery的$.fn
$.fn = {};
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;
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
分解$方法
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.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 ) }
建立元素
$("<p>Hello</p>");
這裡依舊會經過zepto.init的處理,判斷是否具有尖括號(<),有的話便會進入神奇的fragment邏輯建立文件碎片
dom = zepto.fragment(selector, RegExp.$1, context)
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 }
fn的實現
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) }
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 }
事件實現
el.addEventListerner(type, fn, capture);
el.removeEventListerner(type, fn, capture);
兩者引數需要完全一致,而我們的fn很多時候就是個匿名函式甚至是物件,很多時候定義後控制程式碼引用就丟了,我們根本沒法將其保持一致
事件註冊
簡單來說使用zepto繫結事件一般是這樣:
$.click = function (fn) { return this.bind('click', fn); }
事實上他還是呼叫的$.bind實現事件繫結,換個思維方式,其實整個zepto事件實現可以濃縮成這麼幾句話:
var eventSet = { el: {fnType: []} }; function on(type, fn) {} function off(type, fn) {}
$.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) }) }
這裡的event可以是以空格分隔的字串,一般情況下是單一的事件
event => 'mousedown touchstart'
event => 'click'
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)) }) }
第一段程式碼就很重要:
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對應的為一個陣列,這個與繫結先後順序相關
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(' ')} }
handler = { e: 'click', ns: ''//我這裡為null }
// 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繫結的回撥
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 }
觸發事件時他這裡首先會對事件引數event做一次封裝返回,首先將三大事件物件進行新增介面
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)
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) }
$('#wrapper').on('click', '#span', fn);
$('#wrapper').on('click', '#div', fn);
事件移除
$.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) }) }
程式碼比較簡單,可以直接進入remove的邏輯
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取出我們註冊的事件集合,比如:
手勢處理
(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; })();