事件系统
以jquery的版本v3.4.1为例来进行分析.我们首先看下jquery事件绑定常用的方法如下:
绑定原生事件
$("#kkk").on("click",function(){})
//自定义事件
$("#kkk").on("xxx",(e,x)=>{
e.stopPropagation();
console.log(123+x);
})
//触发自定义事件
$("#kkk").trigger("xxx",1212);
//触发原生事件
$("#input").trigger("focus");
如上所写,on既可以绑定原生事件,也可以绑定自定义事件.而trigger既可以触发原生事件,也可以触发自定义事件,接下来我们先看下一下on的源码实现,在追踪on函数实现之前先了解一下jQuery的Data缓存机制
Data缓存
Data是一个构造函数(代码如下),里面有一个属性expando,可以看做成为一个随机生成的字符串作为id的标识.jQuery.expando是一个常量,如果一旦new了一个Data实例那么该实例对象的expando属性就是唯一并且确定的.
在Data的原型上主要有三个方法,分别是cache,add和set方法.我们先看cache方法,owner我们看做成传递过来的dom对象,第一次执行cache函数时dom对象上肯定没有expando属性.没有的话它就会创建一个空对象value,并把expando作为key,value作为值绑定到dom对象上,如图所示.第二次执行该函数就会直接返回value的值.
set函数呢就是获取到上面所说的value这个对象,然后呢给这个对象设置key和value.
get函数通过传入的key值寻找到value这个对象上对应的值.
从这三个实现方法我可以看出来value这个数据对象它并不是存储在Data构造的实例当中而是存储在dom对象上.而cache,set和get函数也只是对dom对象上挂载的vaue对象进行赋值和获取值之类的操作.
function Data() {
this.expando = jQuery.expando + Data.uid++;
}
Data.uid = 1;
Data.prototype = {
cache: function( owner ) {
// Check if the owner object already has a cache
var value = owner[ this.expando ];
// If not, create one
if ( !value ) {
value = {};
if ( acceptData( owner ) ) {
if ( owner.nodeType ) {
owner[ this.expando ] = value;
} else {
Object.defineProperty( owner, this.expando, {
value: value,
configurable: true
} );
}
}
}
return value;
},
set: function( owner, data, value ) {
var prop,
cache = this.cache( owner );
// Handle: [ owner, key, value ] args
// Always use camelCase key (gh-2257)
if ( typeof data === "string" ) {
cache[ camelCase( data ) ] = value;
// Handle: [ owner, { properties } ] args
} else {
// Copy the properties one-by-one to the cache object
for ( prop in data ) {
cache[ camelCase( prop ) ] = data[ prop ];
}
}
return cache;
},
get: function( owner, key ) {
return key === undefined ?
this.cache( owner ) :
owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ];
}
}
on函数实现
先用浏览器调试工具追踪一下on函数的执行过程.
jQuery.event.add函数实现
1.这是执行的第一个函数,继续往下看return旁边的on函数实现
2.在on函数核心的代码是这一句,elem.each是给dom元素同时绑定多个事件,如果用户只用on绑定了一个事件,就只执行一次jQuery.event.add函数了,由此可以看出给元素绑定事件的逻辑全部写在add函数里了.
3.jQuery.event.add函数内的代码量有点多,定义的变量和函数错综复杂第一次看的时候难免会一头雾水,我们现在把焦点投射在核心代码上去一步步窥探它的实现过程.
elem不用多说自然是要绑定事件的主体dom对象.types呢它是绑定的事件类型,此时它会存在两种情况,它可能是原生事件比如click,focus,也可能是用户自定义的事件比如xxx.handler就是用户编写的触发事件后的回调函数.
在这几行代码里我们需要重点关注第5009行,dataPriv是个什么东西.看到没它是Data构造函数的一个实例.并且调用了get方法,由上面的Data缓存机制的描述,又因为是第一次执行不难得出dataPriv就是elem上面挂载的value对象并且还是一个空对象.
这个add函数经过一系列的数据运算操作最后无非是做两件事情,第一个是给dom元素绑定一个事件,第二个给dom对象上的value对象赋值.
给dom元素绑定一个事件,type是事件类型也是用户传入的.但是eventHanlder并不是用户传递的回调函数,而是jQuery.event.dispatch这个函数.
执行完add函数后看下给dom对象挂载的value对象变成了什么样子.它有两个属性handle和events.其中handler指向了上面提到的jQuery.event.dispatch函数.events里面有个click事件类型,说明用户使用on绑定了一个点击事件.里面有type,handler和guid.此处要格外注意这个handler才是用户填写的回调函数,而jQuery.event.dispatch函数里面做的事情就是想办法去执行这个回调函数.
jQuery.event.dispatch函数实现
dispatch函数首先是用原生的事件类型和用户传入的数据组合成一个数组args.随后提取dom对象上面缓存的value对象的handler函数,也就是用户编写的回调函数进行执行,并将args作为参数传入.
以上便是on函数的核心实现流程,jquery并没有在add函数里直接绑定handler函数而是绑定了一个dispatch函数,在disptach里面再去执行这个handler.它为什么要这样绕一层去执行函数呢,因为如此设置便可以轻松的实现自定义事件的绑定和触发了.
自定义事件触发函数trigger
$.trigger的实现,它有两种情况:
第一种是触发自定义事件:$.trigger("xxxx",123);第二个情况触发dom元素的事件:$("input").focus();
1.trigger函数首先会先生成一个自定义的event事件对象,并且把元素赋值上去:event.target = elem;随后呢它会把事件对象和用户传递的参数组合成一个数组data
data = jQuery.makeArray( data, [ event ] );
2.接下来就会出现一个分水岭:
special = jQuery.event.special[ type ] || {};
if (special.trigger.apply( elem, data ) === false ) {
return;
}
如果用户触发的是原生事件,special里面是jQuery全局定义了一些原生事件比如:click,focus,blur.special里面会含有相关的处理函数,此种情况下就会执行special.trigger函数,这个函数的作用只有一个,就是把上面说的value对象的绑定的回调函数handler指向special.trigger里面指定的一个函数,而这个函数里面它的核心代码如下:
if(this.[type]){
this.[type](); //如果dom对象有这个事件直接执行了
}
从这里可以看出来$("#input").trigger("focus")内部执行的其实是input.focus();
3.继续走,这里的代码将元素的所有父元素收集起来,模拟冒泡事件
if ( !rfocusMorph.test( ) ) {
cur = cur.parentNode;
}
for ( ; cur; cur = cur.parentNode ) {
eventPath.push( cur );
}
这里模拟冒泡事件,如果用户调用了e.stopPrapogation()在这里!event.isPropagationStopped()就会跳出循环.
while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {
event.type = type;
handle = dataPriv.get( cur, "handle" );
if ( handle ) {
handle.apply( cur, data );
}
}
执行到这里最终的结果就是执行handle函数,并且把dom对象和[event,用户传递参数]一起给handle,这个hanlder其实就是在上面add函数里面绑定的.
eventHandle = elemData.handle = function( e ) {
return jQuery.event.dispatch.apply( elem, arguments )
};
4.它最后又会回到上面提到的disptach函数,disptach函数主要做的事情就是把dom对象上缓存的handler函数(用户定义的回调函数)取出来执行它,在这里有一点区别的是如果用户触发的是原生事件$.trigger("focus"),那么这个handler早已换成special.trigger里面绑定的函数了,这个过程在上面叙述过。如果是自定义事件,这个handler就是用户定义的函数了.至此最终就能执行到用户定义的那个函数了.