4.4 事件委托:在根部监听末梢 一条候诊队列有一千个条目,每个条目都要响应点击——给每个条目各接一根神经,就是一千根针的工程量;更糟的是 AJAX 刷新后新条目天生没有线,点击没反应。事件委托是这类场景的唯一正解:监听器只绑在容器上,靠事件冒泡把末梢的动静送上来,再按目标筛选分流。本节既是本章的收官,也是前面全部知识的合龙处——冒泡、 、 在这里汇成一套术式。 本节摘要:委托的成立依赖冒泡与 :容器 让容器代管全部条目,命中筛选由 jQuery 用选择器代劳;监听数量从 N 收缩到一,动态新增的元素免接线。不适合委托的无冒泡事件( 、 )仍需直接绑定。 千根针的问题:直接绑定的两宗罪 先把反面的做法摆上台面: 罪状其一,浪费:一千个条目一千份监听器,内存与登记成本随数量线性涨。
一条候诊队列有一千个条目,每个条目都要响应点击——给每个条目各接一根神经,就是一千根针的工程量;更糟的是 AJAX 刷新后新条目天生没有线,点击没反应。事件委托是这类场景的唯一正解:监听器只绑在容器上,靠事件冒泡把末梢的动静送上来,再按目标筛选分流。本节既是本章的收官,也是前面全部知识的合龙处——冒泡、target、closest 在这里汇成一套术式。
本节摘要:委托的成立依赖冒泡与
target:容器on('click', '.item', fn)让容器代管全部条目,命中筛选由 jQuery 用选择器代劳;监听数量从 N 收缩到一,动态新增的元素免接线。不适合委托的无冒泡事件(focus、mouseenter)仍需直接绑定。
先把反面的做法摆上台面:
// 反面教材:逐条接线 $('.item').each(function () { $(this).on('click', function () { $(this).toggleClass('picked'); }); });
罪状其一,浪费:一千个条目一千份监听器,内存与登记成本随数量线性涨。罪状其二,断线:这套绑定只对"绑定时存在"的元素生效,第 6 章实现 AJAX 刷新后,新渲染进来的条目一根线都没有,点击无声无息。4.1 节那桩"点一下触发两次"的悬案,反复重绑的根源也在这里——直接绑定天生扛不住动态内容。

委托形态的 on 只比直接形态多一个参数——夹在事件名与处理器之间的子选择器:
$('#queue').on('click', '.item', function (event) { // this 指向:被点中且匹配 .item 的那个条目 $(this).toggleClass('picked'); });
这行代码的完整语义值得逐字读:把监听器绑在 #queue 上,监听 click,但只有当信号的目标能匹配 .item 时才执行处理器,执行时 this 与 currentTarget 指向匹配的条目。筛选这一步 jQuery 内部替你做了,用的正是 4.2 节那行 $(event.target).closest('.item')。中间那层"点中的其实是条目里的加粗文字"的复杂度,也被 closest 一并消化。
背景:候诊队列由"取号"按钮动态追加,每个条目都支持点击标记完成。操作、结果、解读、变式一次走完:
$(function () { var $queue = $('#queue'); // 委托绑定:全队列只此一份监听,初始化时执行一次 $queue.on('click', '.item', function () { $(this).toggleClass('done'); }); // 取号:不断追加新条目 var seq = 0; $('#refresh').on('click', function () { seq += 1; $queue.append($('<li class="item">新患者 ' + seq + '</li>')); }); });
结果:先点几下取号,再点任何条目(包括刚生成的那条)都能正常切换完成态。解读:条目自己从没有监听器,一切点击都由容器的唯一监听统一处理,所以"新元素没反应"与"重复绑定"两类故障在结构上就不可能出现。变式:条目内部还有"加急"按钮、且点击加急不能触发条目的完成切换——在加急的委托处理器里调 event.stopPropagation() 也拦不住(同一容器的筛选已完成),正确做法是再绑一层更近的委托 $('#queue').on('click', '.urgent-btn', ...) 并在处理器末尾 return false,让更近的委托先一步处理并拦住外层逻辑。
适合委托的场景:同类子元素多(列表、表格、标签组)、内容动态增删、或只是想省监听器。不适合的场景也要背下来——依赖不冒泡事件的交互委托不了:focus 与 blur、mouseenter 与 mouseleave 都不冒泡,信号到不了容器;要统一管理焦点就换冒泡的 focusin 与 focusout,悬停就直接绑或换 mouseover 方案。此外,委托选择器写得过深(链条里四五个层级)会拖慢每次筛选,委托粒度选在"最近的公共容器"最划算。
原生写委托,就是把 jQuery 替你做的筛选亲手写一遍:
document.getElementById('queue').addEventListener('click', function (event) { var item = event.target.closest('.item'); // 从被点中处向上找条目 if (item) { item.classList.toggle('done'); } });
与 jQuery 版逐行对应:closest 负责筛选与归属,判空负责放行无关点击。这段代码是现代原生开发里最常用的模式之一——如果你只打算从 jQuery 带走一样东西去原生世界,就是它。
委托形态的 off 要带上子选择器才算拆对线:
$('#queue').off('click', '.item'); // 只拆委托,别的一概不动 $('#queue').off('click'); // 容器上的 click 全拆,含委托与非委托 $('#queue').off('click.queue-ui'); // 配合命名空间,一次拆一组
最容易踩的坑是只写 off('click') 以为拆掉的是某个委托,实际把容器上非委托的监听也一并端了。配套的换绑场景同理:要给条目换一套点击逻辑,正确次序是"先按原选择器 off,再 on 新的"——不拆就绑等于叠线,4.1 节的重复触发悬案会在委托形态下原样复发。
收益不必凭信念,可以在标本页上量出来:
// 直接绑定版:每个条目一根线 $('.item').on('click', handler); getEventListeners($('#queue')[0]); // 容器上:0 个 getEventListeners($('.item')[0]); // 单个条目上:1 个(乘以条目数) // 委托版:只有容器一根线 $('#queue').on('click', '.item', handler); getEventListeners($('#queue')[0]); // 容器上:1 个 getEventListeners($('.item')[0]); // 条目上:0 个
一千个条目就是一千比一的差距。更重要的是第二条曲线:直接绑定的成本随内容增长持续上升,委托的成本是一条水平线——这也是第 7 章性能清单把"列表交互一律委托"列为止血点的原因。
on(events, selector, handler):监听在容器,筛选按子选择器,this 指向匹配的后代。target 加 closest,原生委托自己写这两步即可。神经接完,这台手术台已经能听会动。下一章给它加上形体语言:显隐、滑动与动画的编排。