jQuery的事件系统是很多人天天在用却很少深究的部分。$('#btn').on('click', handler)一行代码背后,实际经历了事件绑定、事件对象修复、处理队列构建、上下文切换等多个环节,而这些环节的调度核心就是jQuery.event.dispatch。理解了这个函数,事件触发顺序、this指向、事件委托为什么能工作这些问题都会迎刃而解。本文基于jQuery 3.x源码,把这个过程完整拆解一遍。

dispatch在整个事件体系中处于什么位置
先明确一点:dispatch不是给用户调用的API,它是jQuery内部的事件分发引擎。当我们调用on()绑定事件时,jQuery并不是把handler直接挂到元素上,而是把一个统一的入口函数jQuery.event.dispatch作为原生事件的监听器挂载,真正的handler则被存储在内部缓存data_priv(新版本中是Data实例)里。
这样设计的好处很明显:同一个元素同一个事件类型,无论绑定多少个handler,原生层面只有一个监听器,避免重复挂载。当事件发生时,原生监听器触发dispatch,dispatch再从缓存中取出handler列表依次执行。这个“中间层”就是jQuery能实现事件委托、命名空间、一次性事件等高级特性的基础。
dispatch的完整流程可以概括为五步:取出可执行的handler列表、修正事件对象、区分委托与直接绑定、逐个执行handler、处理中断与移除。下面逐段看源码。
事件对象修复与handler队列的构建
dispatch的第一件事是从缓存中取出当前事件类型对应的handler列表:
// 简化后的源码逻辑
dispatch: function( nativeEvent ) {
// 从内部数据缓存中取出该元素该事件类型的处理队列
var handlers = ( dataPriv.get( this, "events" ) || Object.create( null ) )[ event.type ] || [];
// 取出相关的委托元素集合,用于上下文修正
var delegateCount = handlers.delegateCount || 0;
...
}注意这一行dataPriv.get( this, "events" ),this在这里指向绑定了原生监听器的DOM元素,也就是你调用on()的那个元素。handlers数组是按绑定顺序排列的,这直接决定了后面执行的先后——先绑定的先执行,这就是jQuery事件“先绑定先触发”规则的来源。
接着jQuery会调用jQuery.event.fix(nativeEvent)创建一个修复后的事件对象event。为什么要修复?因为IE老版本和标准浏览器的事件对象差异巨大,属性缺失、方法不统一。fix方法会用jQuery.Event构造一个兼容对象,把原生属性拷贝过来,同时补齐preventDefault、stopPropagation等方法,并计算pageX、pageY等派生属性。所以在handler里拿到的event是jQuery包装后的副本,而不是原生对象,这一点在排查问题时很关键。
fix过程中还有个细节:事件对象会被标记isTrigger属性,用于区分是用户真实操作还是trigger()模拟触发。如果你的代码依赖这个判断,就要知道它是在dispatch阶段注入的。
委托事件与直接绑定事件的执行顺序
这是dispatch源码中最精华的部分。jQuery规定:直接绑定的事件先执行,委托事件后执行。实现方式是在构建handlers队列时,把委托类型的handler放在数组尾部,并记录一个分界线delegateCount。看下面的核心片段:
// 简化源码:命中委托元素的判断逻辑
var i = 0, matched, handlerQueue = [];
// 遍历从 delegateCount 到末尾的委托 handler
for ( ; i < handlerQueue前置的直接绑定部分先执行; ) {}
// 实际源码逻辑:从内向外查找匹配的委托元素
var cur = event.target;
while ( cur ) {
for ( matched = 0; matched < handlers.length; matched++ ) {
handler = handlers[ j ];
sel = handler.selector;
// 用 matches 检查当前遍历元素是否命中选择器
if ( sel && sel.match( rneedsContext ) ) {
if ( cur.matches( sel ) ) {
handlerQueue.push( { elem: cur, handler: handler } } );
}
}
}
cur = cur.parentNode; // 沿DOM树向上冒泡查找
}这段代码揭示了委托的本质:dispatch并不是等待原生冒泡一层层触发,而是在监听器触发的那一刻,从event.target开始沿父节点链向上遍历,把所有命中委托选择器的元素收集进handlerQueue。也就是说,jQuery在用户态自己模拟了一遍冒泡过程。这样做的好处是委托handler收到的this可以指向命中选择器的那个元素,而不是绑定on的容器。
举例说明:$('#list').on('click', 'li', handler)中,点击某个li时,handler里的this指向被点击的li元素,event.currentTarget也是li,而event.delegateTarget才是#list。这个上下文切换就是靠构建handlerQueue时记录的elem字段实现的——执行前会执行handler.apply(matchedElem, args),把this动态切换到匹配元素上。
再看顺序问题。handlers数组前段是直接绑定的handler,后段是委托的。执行时先跑直接绑定,再跑委托。如果你在一个既有直接绑定又有委托的场景中发现执行顺序“反直觉”,多半就是踩了这个规则。比如父元素直接绑定的click,反而比子元素上委托给同一个父元素的click先执行,因为直接绑定永远排在前面。
this指向与执行中断的控制细节
dispatch执行handler的核心循环非常简洁:
// 简化源码
i = 0;
while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) {
event.currentTarget = matched.elem;
// 关键:ret = handler.apply( matched.elem, arguments )
ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle ||
jQuery.handle ).apply( matched.elem, args );
if ( ret !== undefined ) {
event.result = ret;
if ( ret === false ) {
event.preventDefault();
event.stopPropagation();
}
}
}两个关键点。第一,handler.apply(matched.elem, args)决定了this的最终归属:直接绑定时matched.elem就是绑定元素,委托时是命中选择器的元素。所以handler里不要用箭头函数,否则this会被词法作用域固定住,拿到的是外层对象而不是DOM元素。第二,ret === false是jQuery的语法糖,handler返回false等价于同时调用preventDefault和stopPropagation,这比原生行为更激进——原生只做preventDefault。
循环条件里的!event.isPropagationStopped()解释了中断机制:handler中调用event.stopPropagation()后,修复后的事件对象会被标记isPropagationStopped: true,循环立刻终止,后续handler不再执行。注意这只会中断jQuery队列内的后续handler,不会影响其他框架绑定的原生监听器。另外stopImmediatePropagation级别更高,它会同时阻止剩余的一切监听,dispatch里通过isImmediatePropagationStopped判断。
还有一个容易被忽略的细节:one()绑定的一次性事件,其移除逻辑也嵌在dispatch的执行路径中——handler执行完毕后检查handleObj.selector与特殊事件类型的teardown钩子,符合条件就自动unbind。理解这一点,你在调试“为什么handler只执行了一次”时就不会一头雾水。
从源码回到实践:三个典型问题的解答
掌握了dispatch的机制后,回头解答几个常见疑问。问题一:同一元素绑定两个click,顺序如何?答案按绑定顺序执行,因为handlers数组按push顺序排列。问题二:为什么委托事件的this不是绑定时写的对象?因为apply时传入的是匹配元素matched.elem。问题三:trigger('click')和真实点击有区别吗?有,模拟触发时事件对象带有isTrigger标记,且不经过原生冒泡,dispatch的模拟逻辑会主动遍历父链调用handler。
最后给一个验证执行顺序的小实验代码,可以直观感受dispatch的调度结果:
var $list = $('#list');
// 直接绑定:进入 handlers 数组前段
$list.on('click', function() {
console.log('1 直接绑定 - this是#list', this.id);
});
// 委托绑定:进入后段,delegateCount 记录分界
$list.on('click', 'li', function() {
console.log('2 委托绑定 - this是li', this.textContent);
// 返回 false 会同时阻止默认行为并中断后续 handler
});
$list.find('li').on('click', function() {
console.log('3 li自身直接绑定 - 最先触发');
});
// 点击li输出顺序:3 -> 1 -> 2输出顺序3、1、2的原因:li自身的原生监听器先于父元素触发(真实冒泡),到了#list的dispatch后,直接绑定(1)排在委托(2)之前。这个例子把原生冒泡和jQuery内部排序两个维度叠加在一起,是检验自己是否真正理解dispatch的最好测试。下次遇到事件顺序或this指向的诡异问题时,不妨直接在源码里翻dispatch,答案基本都在这不到一百行的代码里。
jQuery event dispatchjQuery事件机制事件触发顺序修改时间:2026-09-13 05:36:37