4.1 事件绑定与解绑:接线与拆线 别再往标签里写 了——行内接线把行为焊死在结构上,一个按钮只能挂一个处理器,后来者直接把前任顶掉。现代的事件接线只有一条正路:把监听器绑在元素上,想绑几个绑几个,随时能拆。本节讲的就是这条正路在 jQuery 里的标准姿势 ,以及它的反面 ——会接线更要会拆线,否则重复绑定会把你拖进"点一下触发两次"的经典泥潭。 本节摘要: 是绑定的唯一标准入口,直接形态绑在元素自身,委托形态(带子选择器参数)绑在容器上代管后代; 按事件名、命名空间、处理器引用三档精度拆线;命名空间是防误拆与批量拆的关键设计。 事件信号的传播路线 接线之前得知道信号怎么流。浏览器里每个事件都要走完三段旅程:从文档根向下捕获、在目标元素命中、再向上冒泡回根。
别再往标签里写 onclick="..." 了——行内接线把行为焊死在结构上,一个按钮只能挂一个处理器,后来者直接把前任顶掉。现代的事件接线只有一条正路:把监听器绑在元素上,想绑几个绑几个,随时能拆。本节讲的就是这条正路在 jQuery 里的标准姿势 on,以及它的反面 off——会接线更要会拆线,否则重复绑定会把你拖进"点一下触发两次"的经典泥潭。
本节摘要:
on是绑定的唯一标准入口,直接形态绑在元素自身,委托形态(带子选择器参数)绑在容器上代管后代;off按事件名、命名空间、处理器引用三档精度拆线;命名空间是防误拆与批量拆的关键设计。
接线之前得知道信号怎么流。浏览器里每个事件都要走完三段旅程:从文档根向下捕获、在目标元素命中、再向上冒泡回根。听诊器绑在路线上的哪一站,就听得到哪一段的动静。

记住"绑得越高、管得越宽"这句话,4.4 节的委托术就是把它推到极致。
// 直接形态:绑在元素自身 $('#refresh').on('click', function () { console.log('刷新队列'); }); // 一次接多条神经:空格分隔 $('#search-box').on('focus blur', function () { console.log(this.value); }); // 对象形态:多事件多处理器一次登记 $('.item').on({ click: function () { $(this).toggleClass('picked'); }, dblclick: function () { $(this).remove(); } }); // 委托形态:第二个参数是子选择器,事件由容器代管(4.4 详述) $('#queue').on('click', '.item', function () { $(this).addClass('done'); });
回调里的 this 指向触发时匹配选择器的元素——直接形态里就是绑定的元素本身,委托形态里是被点中的那个后代。要拿事件本尊,就接第一个参数 event,4.2 节整节都在讲它。
拆线的第一课是认清一个事实:off 靠处理器引用对号入座,匿名函数每次都是新身份,天生的拆不掉:
$('#refresh').on('click', function () { console.log('a'); }); $('#refresh').off('click', function () { console.log('a'); }); // 拆不掉:不是同一个引用 function handler() { console.log('a'); } $('#refresh').on('click', handler); $('#refresh').off('click', handler); // 正确拆线:同名同引用
匿名函数时代的老代码靠 unbind 全拆,但那是无差别的——把别人绑的线也拔了。jQuery 的命名空间机制就是为了精细拆线:
$('#refresh').on('click.panel', handler); // 名字后加 .panel,即命名空间 $('#refresh').off('click.panel'); // 只拆自己namespace的,别人绑的 click 不动 $('#refresh').off('.panel'); // 拆掉 panel 名下所有事件
命名空间对off的意义,相当于外科手术里的器械编号——拆台时不靠记忆靠标签,误伤率为零。
背景:列表数据由 AJAX 刷新后重新渲染(第 6 章会实现这个流程),每次渲染完都对"收藏"按钮重新绑定一次点击。症状:刷新过两回之后,点一下收藏,收藏逻辑执行了两遍。
// 病根代码:每次渲染都执行 function bindFav() { $('.fav-btn').on('click', function () { $(this).toggleClass('on'); }); }
操作:在控制台执行 $('.fav-btn').length 后再 $._data($('.fav-btn')[0], 'events') 观察,能看到同一元素上挂着两份 click。结果证实症状来自重复绑定:渲染两次、绑定两次、触发两次。
解读与修复有两条路。路线甲,绑前先拆——$('.fav-btn').off('click').on('click', handler),简单粗暴,适合快速止血;路线乙,根治——把绑定移出渲染函数,只在初始化时用委托绑一次容器(4.4 节的标准用法),渲染多少次都只有一份监听。生产代码推荐乙,甲留作排查其他历史代码的手段。
两个补充用法。其一,只听一次的接线用 one,触发后自动拆线,适合"首次点击弹出引导"这类一次性交互:
$('#first-aid').one('click', function () { console.log('引导只出现这一次'); });
其二,jQuery 3 的 on 支持第四个参数传选项对象,capture: true 可以让监听器站到捕获段——需要在目标处理之前抢先拦截(例如全局的错误兜底、拖拽开始前的准备)时才会用到,日常接线留在冒泡段即可。
| jQuery | 原生 | 备注 |
|---|---|---|
$el.on('click', fn) |
el.addEventListener('click', fn) |
原生支持同事件多监听 |
$el.off('click', fn) |
el.removeEventListener('click', fn) |
同样要求引用一致 |
$el.one('click', fn) |
el.addEventListener('click', fn, { once: true }) |
原生选项直接支持 |
命名空间 .panel |
无对应 | jQuery 独有的精细拆线机制 |
原生阵营没有命名空间,等价的精细拆线只能靠把处理器存进自己的表里来管理——这恰好说明 jQuery 当年解决的是真实痛点。今天写原生代码,推荐用对象映射或集合统一管理监听器,就是在手工复刻命名空间。
on:直接形态绑自身,委托形态给容器加子选择器参数。off 靠处理器引用对号,匿名函数拆不掉;命名空间让拆线可精确到标签级。one;需要在目标之前拦截才考虑 capture: true。addEventListener 语义与 on 基本同构,缺的只是命名空间。线接上了,下一节读信号本身:事件对象里都写了什么,三道闸门分别拦什么。