jQuery源码学习笔记十(改)
继续看jQuery这个经典的基于事件注册的东西。之前jQuery管理回调函数都依赖于一个叫global 的顶层变量,在jQuery1.2.2中(这个版本也是个重要的版本,修正了一百多处bug),搞出缓存系统,也是之前介绍过的jQuery.data。回调函数就不再存储在元素上面,取而代之,在这些元素做一个标识,通过标识获得这些回调函数。毕竟在DOM元素乱加自定义属性是内存泄漏的隐患,并开始支持复合类型(如"mouseover mouseout"),不过最大的改进是对事件对象的修正上。
fix: function (event) { //以前我们在回调函数调用event时,还需要自行做跨游览器处理 //现在我们调用event时,此event已不是浏览器给我们那个,而是一个普通对象 //首先它复制了事件对象所有的成员 var originalEvent = event; event = jQuery.extend({}, originalEvent); //复制成员 //让IE也有preventDefault event.preventDefault = function () { // if preventDefault exists run it on the original event if (originalEvent.preventDefault) originalEvent.preventDefault(); // otherwise set the returnValue property of the original event to false (IE) originalEvent.returnValue = false; }; //让IE也有stopPropagation event.stopPropagation = function () { // if stopPropagation exists run it on the original event if (originalEvent.stopPropagation) originalEvent.stopPropagation(); // otherwise set the cancelBubble property of the original event to true (IE) originalEvent.cancelBubble = true; }; //让IE也有target, //注:IE的document没有srcElement) if (!event.target) event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either // safari连文本节点也是事件源 if (event.target.nodeType == 3) event.target = originalEvent.target.parentNode; // Add relatedTarget, if necessary //让IE也relatedTarget,不过这属性在火狐中也是在mouseover与mouseout事件中有效 if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; // Calculate pageX/Y if missing and clientX/Y available //为IE添加pageX/Y,但并不准确,没有修正2px bug if (event.pageX == null && event.clientX != null) { var doc = document.documentElement, body = document.body; event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0); event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0); } // Add which for key events //为IE添加which if (!event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode)) event.which = event.charCode || event.keyCode; // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey; // Add which for click: 1 == left; 2 == middle; 3 == right // Note: button is not normalized, so don't use it //设置左中右键 if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0))); return event; },
嘛,现在我们看现在最流行的版本吧。它在生成那个冒牌事件对象的手法做了一些改进,分两步走,首先用jQuery.Event生成一个对象,让它获得W3C的两个方法与一个stop方法,然后是逐加它感兴趣的属性。
jQuery.event.add适用于为元素的某个事件添加回调函数。
jQuery.event = { //添加绑定函数 add: function (elem, types, handler, data) { //不明白为什么连文本节点也在列,如果是注释节点则是IE的getElementsByTagName引起的 if (elem.nodeType == 3 || elem.nodeType == 8) return; //处理window对象,因为window有setInterval方法,但是在多框架结构中,如el.contentWindow //el.frameElement,这些el也有setInterval方法,但它们并不是最顶层的window对象 if (elem.setInterval && elem != window) elem = window; //给每个绑定事件分配一个UUID if (!handler.guid) handler.guid = this.guid++; //如果有data则用proxy把它wrapper一下,然后把data依附其上 //严重怀疑data是冒泡参数 if (data !== undefined) { // Create temporary function pointer to original handler var fn = handler; // Create unique handler function, wrapped around original handler handler = this.proxy(fn); // Store data in unique handler handler.data = data; } // Init the element's event structure //根据elem的UUID在jQuery.cache中设置一个events对象 var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}), //根据elem的UUID在jQuery.cache中设置一个handle对象,并存储一个函数进去 handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function () { //如果event.triggered为true立即执行此函数,相当于IE的fireEvent return typeof jQuery !== "undefined" && !jQuery.event.triggered ? //arguments.callee.elem即handle.elem jQuery.event.handle.apply(arguments.callee.elem, arguments) : undefined; }); // Add elem as a property of the handle function // This is to prevent a memory leak with non-native // event in IE. handle.elem = elem; // Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); // 同时处理两种事件,主要是用于模拟CSS的:hover伪类 jQuery.each(types.split(/\s+/), function (index, type) { // Namespaced event handlers var namespaces = type.split("."); type = namespaces.shift(); //相当于handler.type = "mouseout.mouseover" handler.type = namespaces.slice().sort().join("."); // Get the current list of functions bound to this event var handlers = events[type]; if (jQuery.event.specialAll[type]) jQuery.event.specialAll[type].setup.call(elem, data, namespaces); // Init the event handler queue if (!handlers) { handlers = events[type] = {}; // Check for a special event handler // Only use addEventListener/attachEvent if the special // events handler returns false //只有执行domReady时才用DOM2的API(最新版还有其他函数) if (!jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem, data, namespaces) === false) { // Bind the global event handler to the element if (elem.addEventListener) elem.addEventListener(type, handle, false); else if (elem.attachEvent) elem.attachEvent("on" + type, handle); } } // Add the function to the element's handler list handlers[handler.guid] = handler; // Keep track of which events have been used, for global triggering jQuery.event.global[type] = true; }); // Nullify elem to prevent memory leaks in IE elem = null; }, guid: 1, global: {},
jQuery.event.add适用于为元素的某个事件移除指定的回调函数。
// Detach an event or set of events from an element remove: function (elem, types, handler) { // don't do events on text and comment nodes if (elem.nodeType == 3 || elem.nodeType == 8) return; //取得本炒绑定在元素上的所有事件 var events = jQuery.data(elem, "events"), ret, index; if (events) { // Unbind all events for the element if (types === undefined || (typeof types === "string" && types.charAt(0) == ".")) for (var type in events) //移除所有类型的事件 this.remove(elem, type + (types || "")); else { // types is actually an event object here if (types.type) { handler = types.handler; types = types.type; } // Handle multiple events seperated by a space // jQuery(...).unbind("mouseover mouseout", fn); jQuery.each(types.split(/\s+/), function (index, type) { // Namespaced event handlers var namespaces = type.split("."); type = namespaces.shift(); var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)"); if (events[type]) { //移除某一类型 // remove the given handler for the given type if (handler) //移除这个回调函数 delete events[type][handler.guid]; // remove all handlers for the given type else for (var handle in events[type]) // Handle the removal of namespaced events if (namespace.test(events[type][handle].type)) delete events[type][handle]; if (jQuery.event.specialAll[type]) jQuery.event.specialAll[type].teardown.call(elem, namespaces); // remove generic event handler if no more handlers exist for (ret in events[type]) break; if (!ret) { if (!jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem, namespaces) === false) { if (elem.removeEventListener) elem.removeEventListener(type, jQuery.data(elem, "handle"), false); else if (elem.detachEvent) elem.detachEvent("on" + type, jQuery.data(elem, "handle")); } ret = null; delete events[type]; } } }); } // Remove the expando if it's no longer used for (ret in events) break; if (!ret) { //移除组存体上的相关数据 var handle = jQuery.data(elem, "handle"); if (handle) handle.elem = null; jQuery.removeData(elem, "events"); jQuery.removeData(elem, "handle"); } } },
jQuery.event.trigger允许用户以编程方式执行指定元素上某种事件类型的所有回调函数,并允许通过事件冒泡,触发上层元素更多的同类型事件发生。相当于IE的fireEvent
trigger: function (event, data, elem, bubbling) { //这时的event可能已经改造过了 // Event object or event type var type = event.type || event; if (!bubbling) { //如果不能冒泡 //如果传入的event不是加工的冒牌事件对象,让它变成冒牌的 event = typeof event === "object" ? // jQuery.Event object event[expando] ? event : //如果是通事件代理,则用type初始化一个假冒的事件对象,并把子元素的假冒事件对象的属性继承给它 jQuery.extend(jQuery.Event(type), event) : // Just the event type (string) jQuery.Event(type); if (type.indexOf("!") >= 0) { //判定是否有NOT操作符 //目的是执行非传入事件类型的其他事件类型的回调函数 event.type = type = type.slice(0, -1); event.exclusive = true; } // Handle a global trigger if (!elem) { // Don't bubble custom events when global (to avoid too much overhead) event.stopPropagation(); //防止事件冒泡,因为是冒泡事件对象,所以IE下也能调用此方法 // Only trigger if we've ever bound an event for it if (this.global[type]) jQuery.each(jQuery.cache, function () { if (this.events && this.events[type]) jQuery.event.trigger(event, data, this.handle.elem); }); } // Handle triggering a single element // don't do events on text and comment nodes if (!elem || elem.nodeType == 3 || elem.nodeType == 8) return undefined; //清除event.result,它用于阻止事件冒泡与默认事件的发生 event.result = undefined; event.target = elem;//改写事件源对象 // Clone the incoming data, if any data = jQuery.makeArray(data); data.unshift(event); } event.currentTarget = elem; // Trigger the event, it is assumed that "handle" is a function var handle = jQuery.data(elem, "handle"); if (handle) handle.apply(elem, data); // Handle triggering native .onfoo handlers (and on links since we don't call .click() for links) //处理点击链接的情形,如果回调函数return false,那就让它不能冒泡与执行默认行为 if ((!elem[type] || (jQuery.nodeName(elem, 'a') && type == "click")) && elem["on" + type] && elem["on" + type].apply(elem, data) === false) event.result = false; // Trigger the native events (except for clicks on links) //处理直接绑定在元素的事件 if (!bubbling && elem[type] && !event.isDefaultPrevented() && !(jQuery.nodeName(elem, 'a') && type == "click")) { this.triggered = true; try { elem[type](); // prevent IE from throwing an error for some hidden elements } catch(e) {} } this.triggered = false; //让它冒泡执行其祖先元素上的同类型事件 if (!event.isPropagationStopped()) { var parent = elem.parentNode || elem.ownerDocument; if (parent) jQuery.event.trigger(event, data, parent, true); } },
当用户在页面点击或移动鼠标,触发某元素上的回调函数时,jQuery对此回调函数进行一些改造,主要是对事件对象的处理,把原事件对象或window.event封进一个冒假事件对象中,并以它作为回调函数的第一个参数传入。
handle: function (event) { // returned undefined or false var all, handlers; //设置一个假冒的事件对象,屏蔽浏览器的差异 event = arguments[0] = jQuery.event.fix(event || window.event); event.currentTarget = this;//设置事件执行对象 // Namespaced event handlers var namespaces = event.type.split("."); event.type = namespaces.shift(); // Cache this now, all = true means, any handler all = !namespaces.length && !event.exclusive; var namespace = RegExp("(^|\\.)" + namespaces.slice().sort().join(".*\\.") + "(\\.|$)"); handlers = (jQuery.data(this, "events") || {})[event.type]; //取得此类型的所有回调函数 for (var j in handlers) { var handler = handlers[j]; // Filter the functions by class if (all || namespace.test(handler.type)) { // Pass in a reference to the handler function itself // So that we can later remove it event.handler = handler; event.data = handler.data; //ret用于判定阻止冒泡与冒泡与执行默认行为 var ret = handler.apply(this, arguments); if (ret !== undefined) { event.result = ret; if (ret === false) { event.preventDefault(); event.stopPropagation(); } } //用于防止同类型事件继续执行 if (event.isImmediatePropagationStopped()) break; } } },
对事件对象进行封装,用一个普通的对象包裹它,并让它拥有原事件所有的行为与属性。
props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), fix: function (event) { //此函数是用于生成冒牌事件对象,屏蔽IE与W3C的差异,见前面的 if (event[expando]) return event; // store a copy of the original event object // and "clone" to set read-only properties var originalEvent = event; event = jQuery.Event(originalEvent);//添加方法 for (var i = this.props.length, prop; i;) {//添加属性 prop = this.props[--i]; event[prop] = originalEvent[prop]; } // Fix target property, if necessary if (!event.target) event.target = event.srcElement || document; // Fixes #1925 where srcElement might not be defined either // check if target is a textnode (safari) if (event.target.nodeType == 3) event.target = event.target.parentNode; // 不准确,在FF中会返回错误的XUI Element if (!event.relatedTarget && event.fromElement) event.relatedTarget = event.fromElement == event.target ? event.toElement : event.fromElement; // 不准确,没有处理2px bug if (event.pageX == null && event.clientX != null) { var doc = document.documentElement, body = document.body; event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0); event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0); } // Add which for key events if (!event.which && ((event.charCode || event.charCode === 0) ? event.charCode : event.keyCode)) event.which = event.charCode || event.keyCode; // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) if (!event.metaKey && event.ctrlKey) event.metaKey = event.ctrlKey; // Add which for click: 1 == left; 2 == middle; 3 == right // Note: button is not normalized, so don't use it if (!event.which && event.button) event.which = (event.button & 1 ? 1 : (event.button & 2 ? 3 : (event.button & 4 ? 2 : 0))); return event; },
其他旮旯,没多看。live好像是用于事件代理的。
proxy: function (fn, proxy) { //生成一个包装了原回调函数的函数 proxy = proxy || function () { return fn.apply(this, arguments); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || proxy.guid || this.guid++; // So proxy can be declared as an argument return proxy; }, special: { //主要用于domReady ready: { // Make sure the ready event is setup setup: bindReady, teardown: function () {} } }, specialAll: { live: { setup: function (selector, namespaces) { jQuery.event.add(this, namespaces[0], liveHandler); }, teardown: function (namespaces) { //???用于unload if (namespaces.length) { var remove = 0, name = RegExp("(^|\\.)" + namespaces[0] + "(\\.|$)"); jQuery.each((jQuery.data(this, "events").live || {}), function () { if (name.test(this.type)) remove++; }); if (remove < 1) jQuery.event.remove(this, namespaces[0], liveHandler); } } } } };