导读:本期聚焦于弦宿​创作的《如何通过jQuery.event.dispatch源码理解事件触发的执行顺序与上下文》,敬请观看详情。为什么给同一个元素绑定的多个jQuery事件处理器,有的先执行有的后执行?委托事件为什么总是在直接绑定事件之后触发?被劫持的this究竟指向谁?这些问题的答案都藏在jQuery.event.dispatch这个核心函数里。本文从源码入手,逐步拆解dispatch的完整执行流程,包括事件对象修复、事件队列构建、 delegateCount 的判断逻辑、this指向的动态修正以及 stopPropagation 对执行链的影响。读完之后你就能准确预判任意事件绑定组合下的触发顺序,也能看懂 jQuery 内部对原生事件对象做包装处理的思路,面试或排查线上事件问题时都更有底气。

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

如何通过jQuery.event.dispatch源码理解事件触发的执行顺序与上下文

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构造一个兼容对象,把原生属性拷贝过来,同时补齐preventDefaultstopPropagation等方法,并计算pageXpageY等派生属性。所以在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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/55796.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。