Event源码
Event模块的设计思路
1、$.event.add(elem,types,handler,data,selector) 添加事件回调函数:
构造Data对象实现,其中,elemData.handle属性存放的回调函数通过elem.addEventListener方法绑定到元素上,页面事件触发时,执行该回调函数;elemData.events属性以对象形式存放事件类型、命名空间、捕获元素、回调的guid、回调函数的其他信息等。同时在$.event.dispatch()方法中,elemData.events对象用于生成elemData.handle回调函数的内部代码。
因为Data对象附着在elem元素上,可通过dataPriv.get(this,"events")方式获取,并进行后续使用、添加或移除操作。
// 添加绑定事件 add:function(elem,types,handler,data,selector){ var handleObjIn, eventHandle, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData=dataPriv.get(elem);// 创建或获取Data对象,附着在jquery对象elem的属性中 if ( !elemData ){// 文本、注释节点不能设置Data对象 return; } if ( handler.handler ){// 回调函数设置为对象形式,handler属性为函数,selector为捕获节点 handleObjIn=handler; handler=handleObjIn.handler; selector=handleObjIn.selector; } // jQuery.find.matchesSelector(elem,expr) 判断元素elem是否满足表达式expr if ( selector ){ jQuery.find.matchesSelector(documentElement,selector); } // 绑定函数添加guid属性,以便解绑时确认删除绑定事件 if ( !handler.guid ){ handler.guid=jQuery.guid++; } // elemData.events未定义设置为空对象,通过events获取elemData.events;elemData.handle相同 // elemData.events[type]存储handleObj,包含事件类型、上下文、选择器、回调函数相关数据 // hanlers获取elemData.events[type]添加回调函数 // elemData.handle存储回调函数的触发方法dispatch,接着通过elemData.events获取回调 if ( !(events=elemData.events) ){ events=elemData.events={}; } if ( !(eventHandle=elemData.handle) ){ // jQuery.event.dispatch获取原生语句addEventListener绑定的回调函数,并且添加到Data对象中 // 传参为on语法回调函数的参数arguments,event对象内部构 eventHandle=elemData.handle=function(e){ // 页面尚未完成加载或者调用$ele.trigger()方法时,不用原生语句添加事件 return typeof jQuery!=="undefined" && jQuery.event.triggered!==e.type ? jQuery.event.dispatch.apply(elem,arguments) : undefined; }; } types=( types || "" ).match(rnotwhite) || [""];// 多事件以空格分割 t=types.length; while (t--){ // 事件类型配置项拆分所属事件类型名和命名空间 tmp=rtypenamespace.exec(types[t]) || []; type=origType=tmp[1];// 事件类型 namespaces=( tmp[2] || "" ).split(".").sort();// 命名空间 if ( !type ){ continue; } special=jQuery.event.special[type] || {};// 特殊事件focusin等经jquery转化成focus输出 // 获取浏览器原生的事件focusin、focusout等 type=(selector ? special.delegateType : special.bindType) || type; special=jQuery.event.special[type] || {}; // handleObj包含事件类型、回调函数等信息,存储在elemData.events[type]数组中 handleObj=jQuery.extend({ type:type,// 浏览器支持的事件类型 origType:origType,// jquery支持的事件类型 data:data,// 附带的数据 handler:handler,// 事件的回调函数 guid:handler.guid,// 回调函数的guid selector:selector,// 捕获的节点 needsContext:selector && jQuery.expr.match.needsContext.test(selector), namespace:namespaces.join(".")// 命名空间 },handleObjIn);// handleObjIn回调函数相关数据 // elemData.events[type]未定义设置为空数组,通过handlers获取elemData.events[type] // type为浏览器原生事件类型 if ( !(handlers=events[type]) ){ handlers=events[type]=[]; handlers.delegateCount=0; // special.setup方法以使focus、blur事件通过向文档节点挂载$ele.trigger实现获得、失去焦点 // addEventListener原生语句绑定事件,事件捕获功能由浏览器原生语句支持 if ( !special.setup || special.setup.call(elem,data,namespaces,eventHandle)===false ){ if ( elem.addEventListener ){// 浏览器原生语句绑定事件 elem.addEventListener(type,eventHandle); } } } // 默认special.add均为false,add方法对Data对象中回调队列进行改写 if ( special.add ){ special.add.call(elem,handleObj); if ( !handleObj.handler.guid ){ handleObj.handler.guid=handler.guid; } } // 有捕获元素,回调队列前插入,无则队列后添加;调用回调时dispatch、handlers方法获取handlers if ( selector ){ handlers.splice(handlers.delegateCount++,0,handleObj); } else { handlers.push(handleObj); } jQuery.event.global[type]=true; } },
2、$.event.dispatch(nativeEvent) 构造元素绑定的回调函数:
通过Dom接口addEventLIstener添加到elem元素上,当浏览器点击、悬停等事件触发时执行回调函数。
构造回调函数过程中,首先浏览器原生事件对象nativeEvent通过jQuery.event.fix方法封装为jQuery特有的event对象,且作为回调的首参;其余参数在add方法调用dispatch时带入。
回调函数执行前先执行sepcial.preDispatch函数,执行完成后执行sepcial.postDispatch函数。
通过$.event.handlers获取触发事件元素及其父元素的data对象上绑定的回调函数和执行上下文,通过event.isPropagationStoped方法判断是否阻止向上冒泡,以及event.isImmediatePropagationStoped方法旁观是否阻止同一元素的其他挂载函数,否则通过比较命名空间执行特殊的sepcial[origType].handle回调、或者$.event.handlers取出的回调,回调返回为false时阻止默认事件以及向上冒泡。
// 事件触发时获取回调函数队列并处理执行 // 首先执行有捕获元素selector设置的回调函数,再执行没有捕获元素的回调函数 // event.stopPropagation阻止向上冒泡触发事件,event.stopImmediatePropagation阻止元素后续的绑定事件 // dispatch方法内部this关键字为绑定事件元素 dispatch:function(nativeEvent){ // 获取jquery包装的event对象 var event=jQuery.event.fix(nativeEvent); var i, j, ret, matched, handleObj, handlerQueue, args=new Array(arguments.length), handlers=( dataPriv.get(this,"events") || {} )[event.type] || [],// 获取绑定函数 special=jQuery.event.special[event.type] || {}; args[0]=event; for ( i=1; i<arguments.length; i++ ) { args[i]=arguments[i];// 用户配置的回调函数参数 } event.delegateTarget=this;// 绑定事件的节点 // special.preDispatch、special.postDispatch钩子进行预处理、回调后处理 if ( special.preDispatch && special.preDispatch.call(this,event)===false ){ return; } // 获取Data对象中回调函数队列,及回调函数执行的上下文 handlerQueue=jQuery.event.handlers.call(this,event,handlers); i=0; // event.isPropagationStopped返回真值阻止事件冒泡,只执行捕获或绑定元素的事件 while ( ( matched=handlerQueue[i++] ) && !event.isPropagationStopped() ){ event.currentTarget=matched.elem; j=0; // event.isImmediatePropagationStopped返回真值阻止当前元素后续的绑定事件 while ( (handleObj=matched.handlers[j++]) && !event.isImmediatePropagationStopped() ){ if ( !event.rnamespace || event.rnamespace.test(handleObj.namespace) ){ event.handleObj=handleObj; event.data=handleObj.data; // 执行$ele.on方法添加的回调函数,this关键字为绑定事件元素,首参为event ret=( (jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler ) .apply(matched.elem,args); // $ele.on方法返回false阻止默认事件以及冒泡行为 if ( ret!==undefined ){ if ( ( event.result=ret )===false ){ event.preventDefault(); event.stopPropagation(); } } } } } if ( special.postDispatch ){ special.postDispatch.call(this,event); } return event.result; },
3、$.event.handlers(event,handlers)取出元素data对象中的回调函数:
获取当前元素及其父元素data中的回调函数及回调上下文。
// 获取绑定元素Data对象中存储的回调函数,以及该回调执行的上下文,即期望的或绑定事件的节点元素 // 返回handlerQueue数组个数由回调函数上下文个数决定,即捕获元素selector个数+1 // handlers方法内部this关键字为绑定事件元素 handlers:function(event,handlers){ var i, matches, sel, handleObj, handlerQueue=[], delegateCount=handlers.delegateCount,// $ele.on方法设置捕获元素的次数 cur=event.target; // Support: IE <=9 // Find delegate handlers // Black-hole SVG <use> instance trees (#13180) // // Support: Firefox <=42 // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) if ( delegateCount && cur.nodeType && ( event.type!=="click" || isNaN(event.button) || event.button<1 ) ){ for ( ; cur!==this; cur=cur.parentNode || this ){ // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) if ( cur.nodeType===1 && ( cur.disabled!==true || event.type!=="click" ) ){ matches=[]; for ( i=0; i<delegateCount; i++ ){ handleObj=handlers[i]; sel=handleObj.selector+" "; if ( matches[sel]===undefined ){ matches[sel]=handleObj.needsContext ? jQuery(sel,this).index(cur)>-1 :// sel为数组形式,cur为其中一项 jQuery.find(sel,this,null,[cur]).length; } if ( matches[sel] ){ matches.push(handleObj); } } if ( matches.length ){ handlerQueue.push( {elem:cur,handlers:matches} ); } } } } // $ele.on、$ele.one方法未添加捕获元素,获取绑定的回调函数,以及该回调执行的上下文,即绑定元素 if ( delegateCount<handlers.length ){ handlerQueue.push( {elem:this, handlers:handlers.slice(delegateCount)} ); } return handlerQueue; },
4、$.event.remove(elem,types,handler,selector,mappedTypes)移除data对象的回调函数:
通过引用类型移除元素data对象中的回调函数,清空完毕后释放内存,并用浏览器原生语句removeEventListener方法解绑回调。
// 删除绑定事件,由回调guid、捕获元素、事件类型查询绑定事件并撤销 remove:function(elem,types,handler,selector,mappedTypes){ var j, origCount, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, elemData=dataPriv.hasData(elem) && dataPriv.get(elem); if ( !elemData || !( events=elemData.events ) ){ return; } types=( types || "" ).match(rnotwhite) || [""]; t=types.length while ( t-- ){ tmp=rtypenamespace.exec(types[t]) || []; type=origType=tmp[1]; namespaces=( tmp[2] || "" ).split(".").sort(); // 当type赋值为空,解绑相同命名空间namespaces下所有事件 if ( !type ){ for ( type in events ){ jQuery.event.remove(elem,type+types[t],handler,selector,true ); } continue; } special=jQuery.event.special[type] || {}; type=( selector ? special.delegateType : special.bindType ) || type; handlers=events[type] || []; tmp=tmp[2] && new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)"); origCount=j=handlers.length; while ( j-- ){ handleObj=handlers[j];// 获取Data对象中的回调函数队列 if ( ( mappedTypes || origType===handleObj.origType ) &&// 相同事件类型或所有 ( !handler || handler.guid===handleObj.guid ) &&// 相同绑定函数或所有 ( !tmp || tmp.test(handleObj.namespace) ) &&// 相同命名空间或所有 ( !selector || selector===handleObj.selector ||// 相同捕获元素或所有 selector==="**" && handleObj.selector ) ){ handlers.splice(j,1); if ( handleObj.selector ){ handlers.delegateCount--; } if ( special.remove ){ special.remove.call(elem,handleObj); } } } // special.teardown方法更新文档节点绑定focus、blur事件的数量,没有事件时解绑函数 if ( origCount && !handlers.length ){ if ( !special.teardown || special.teardown.call(elem,namespaces,elemData.handle)===false ){ jQuery.removeEvent(elem,type,elemData.handle); } delete events[type]; } } if ( jQuery.isEmptyObject( events ) ){// 清空Data对象,节省内存 dataPriv.remove(elem,"handle events"); } },
5、$.event.trigger(event,data,elem,onlyHandlers) 以jQuery方式触发绑定函数:
先根据传参event是否jQuery封装的event对象,若不是,通过传参event获取jQuery封装的event对象。该event对象添加isTrigger属性,为真值时使用$.event.trigger触发事件,为否值时由浏览器点击等事件触发。
若当前事件类型的sepcial.trigger函数存在,执行该函数并返回,已知focus/blur事件为此种情况,仅得到或失去焦点。其他情况,先以eventPath数组形式获取存储向上冒泡的元素路径,接着获取data对象中各元素的回调函数并执行,以及元素路径上eventPath[i][ontype]用浏览器原生语句绑定在元素上的回调函数(该回调返回否值,阻止浏览器默认事件触发),接着根据sepcial._default函数返回值为否的情况,触发调用trigger方法元素elem[ontype]函数。
// onlyHandlers阻止向上冒泡 trigger:function(event,data,elem,onlyHandlers){ var i, cur, tmp, bubbleType, ontype, handle, special, eventPath=[elem || document], type=hasOwn.call(event,"type") ? event.type : event, namespaces=hasOwn.call(event,"namespace") ? event.namespace.split(".") : []; cur=tmp=elem=elem || document; if ( elem.nodeType===3 || elem.nodeType===8 ){// 文本节点和注释节点不能触发事件 return; } // focus/blur事件触发过程中,focusin/focusout事件不触发 if ( rfocusMorph.test(type+jQuery.event.triggered) ){ return; } if ( type.indexOf(".")>-1 ){ namespaces=type.split("."); type=namespaces.shift(); namespaces.sort(); } ontype=type.indexOf(":")<0 && "on"+type; // 获取或创建jquery封装的event对象 event=event[jQuery.expando] ? event : new jQuery.Event(type,typeof event==="object" && event); // event.isTrigger为真值时,由用户调用$ele.trigger方法触发,否则为浏览器事件自动出阿发 event.isTrigger=onlyHandlers ? 2 : 3; event.namespace=namespaces.join("."); // 以"."号分割的字符串中包含namespaces以"."号分割后的各子字符串 event.rnamespace=event.namespace ? new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)") : null; event.result=undefined; if ( !event.target ) { event.target=elem; } data=data==null ? [event] : jQuery.makeArray(data,[event]);// 作为参数传入回调函数 // special.trigger输入框获得focus或失去blur焦点,复选框选中click事件触发,阻止向上冒泡 special=jQuery.event.special[type] || {}; if ( !onlyHandlers && special.trigger && special.trigger.apply(elem,data)===false ){ return; } // eventPath添加向上冒泡元素路径 if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow(elem) ){ bubbleType=special.delegateType || type; if ( !rfocusMorph.test(bubbleType+type) ){// rfocusMorph函数校验blur、focus返回true cur=cur.parentNode; } for ( ; cur; cur=cur.parentNode ){ eventPath.push(cur); tmp=cur; } if ( tmp===( elem.ownerDocument || document ) ){ eventPath.push( tmp.defaultView || tmp.parentWindow || window ); } } // 执行回调函数,按条件执行浏览器原生的事件处理函数 i=0; while ( ( cur=eventPath[i++] ) && !event.isPropagationStopped() ){ event.type=i>1 ? bubbleType : special.bindType || type; // 获取绑定回调函数并执行,以cur作为上下文 handle=( dataPriv.get(cur,"events") || {} )[event.type] && dataPriv.get(cur,"handle"); if ( handle ){ handle.apply(cur,data); } // 获取并执行浏览器原生事件cur[ontype],该事件函数返回否值时,阻止浏览器默认事件执行 handle=ontype && cur[ontype]; if ( handle && handle.apply && acceptData(cur) ){ event.result=handle.apply(cur,data); if ( event.result===false ){ event.preventDefault(); } } } event.type=type; if ( !onlyHandlers && !event.isDefaultPrevented() ){ // special._default返回为真值时,不执行elem[type] // special._default为否则或其返回为否值时,执行elem[type] if ( ( !special._default || special._default.apply(eventPath.pop(),data)===false ) && acceptData(elem) ){ // 获取并执行浏览器原生事件elem[type],执行过程中缓存elem[ontype] if ( ontype && jQuery.isFunction(elem[type]) && !jQuery.isWindow(elem) ){ tmp=elem[ontype]; if ( tmp ){ elem[ontype]=null; } jQuery.event.triggered=type; elem[type](); jQuery.event.triggered=undefined; if ( tmp ){ elem[ontype]=tmp; } } } } return event.result; },
5、jQuery封装的event对象,构造函数为$.Event:
继承原生event对象的属性如offsetX等,按键which属性通过改写后获得。
添加preventDefault方法用于阻止默认事件触发,isDefaultPrevented用于检测默认事件是否不予触发。
stopPropagation方法用于阻止事件冒泡,isPropagationStopped用于检测向上冒泡是否已阻止。
stopImmediatePropagation方法用于阻止同一元素的后续事件触发,isImmediatePropagationStopped检测是否阻止。
// 调用e.preventDefault方法时,改变状态返回函数e.isDefaultPrevented的输出值 // 从而对回调函数产生影响,stopPropagation、stopImmediatePropagation方法相同 jQuery.Event.prototype={ constructor:jQuery.Event, isDefaultPrevented:returnFalse, isPropagationStopped:returnFalse, isImmediatePropagationStopped:returnFalse, isSimulated:false preventDefault:function(){// 阻止默认事件 var e=this.originalEvent; this.isDefaultPrevented=returnTrue; if ( e && !this.isSimulated ){ e.preventDefault();// 调用浏览器原生语句阻止默认事件 } }, stopPropagation:function(){// 阻止向上冒泡 var e=this.originalEvent; this.isPropagationStopped=returnTrue; if ( e && !this.isSimulated ){ e.stopPropagation(); } }, stopImmediatePropagation:function(){// 阻止后续同类事件执行 var e=this.originalEvent; this.isImmediatePropagationStopped=returnTrue; if ( e && !this.isSimulated ){ e.stopImmediatePropagation(); } this.stopPropagation(); } }; // 回调函数参数event属性设置取值、赋值,模块内立即执行 jQuery.each({ altKey: true, bubbles: true, cancelable: true, changedTouches: true, ctrlKey: true, detail: true, eventPhase: true, metaKey: true, pageX: true, pageY: true, shiftKey: true, view: true, "char": true, charCode: true, key: true, keyCode: true, button: true, buttons: true, clientX: true, clientY: true, offsetX: true, offsetY: true, pointerId: true, pointerType: true, screenX: true, screenY: true, targetTouches: true, toElement: true, touches: true, which: function( event ) { var button = event.button; // Add which for key events if ( event.which==null && rkeyEvent.test(event.type) ){ return event.charCode!=null ? event.charCode : event.keyCode; } // Add which for click: 1 === left; 2 === middle; 3 === right if ( !event.which && button!==undefined && rmouseEvent.test(event.type) { return ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); } return event.which; } },jQuery.event.addProp);
6、$.event.sepcial特殊事件处理:
special:{ /** * sepcial[type].trigger: $ele.trigger触发事件时使用,避过注册事件的执行和冒泡 * 只执行sepcial[type].trigger方法,包含得到失去焦点、复选框点选,focus、blur、click * sepcial[type].setup: 执行$ele.on方法时调用,支持事件类型为focus、blur * 将jQuery.event.simulate方法作为回调绑定在文档节点上, * 通过simulate触发jQuery.event.trigger方法,进而触发special.trigger执行 * event.target单纯获得、失去焦点,情形类同没有冒泡效果的事件 * sepcial[type].teardown: 执行$ele.remove方法时调用,支持事件类型为focus、blur * 文档节点缓存focus、blur事件数减1,文档节点解绑jQuery.event.trigger方法 * sepcial[type]._default: $ele.trigger触发事件时调用,返回为真,阻止ele[event.type]执行 * 当ele为a标签时,不触发ele["click"]函数跳链接 * special[type].preDispatch:绑定函数执行完成前调用 * special[type].postDispatch:绑定函数执行完成后调用 * special[type].remove: 移除回调函数时执行 * special[type]._default:返回真值阻止默认事件,已知a标签点击时不跳连接 */ load:{ // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus:{ // $.trigger("focus")阻止事件向上冒泡,只实现元素获得焦点 trigger:function(){ if ( this!==safeActiveElement() && this.focus ){ this.focus(); return false; } }, // focus类型注册的事件添加到elemData.events.focusin对象中,触发focus事件时也不取出 delegateType:"focusin" }, blur: { // $.trigger("focus")阻止事件向上冒泡,只实现元素失去焦点 trigger:function(){ if ( this===safeActiveElement() && this.blur ){ this.blur(); return false; } }, delegateType:"focusout" }, click:{ // $.trigger("click")阻止事件向上冒泡,只实现复选框元素点中 trigger:function(){ if ( this.type==="checkbox" && this.click && jQuery.nodeName(this,"input") ){ this.click(); return false; } }, // $ele.trigger方法触发时间,ele为a标签时返回为真,阻止ele["click"]执行,不触发跳链接动作 _default:function(event){ return jQuery.nodeName(event.target,"a"); } }, beforeunload:{// 离开页面事件,绑定函数返回值作为页面alert弹窗提示,原理不明? postDispatch:function(event){ if ( event.result!==undefined && event.originalEvent ){ event.originalEvent.returnValue=event.result; } } } }