JS公共方法加载多次导致事件重复触发
多模块项目里,公共 JS 文件在各模块 header 都引入了,页面 load 时会重复加载。事件绑定多次后,点击一次按钮触发多次事件,偶数次表现为失效。解决方法是公共 JS 只在入口页面引入一次,避免重复加载。
有个多模块共用一个入口的老项目,各模块代码在同一个工程里,但彼此独立。
某天发现一个怪事:页面上的折叠按钮全失效了。点一下,没反应。
更怪的是,不是每次都失效,有时候好的,有时候不动。这种"薛定谔的 bug"最磨人。
查了半天才发现,根子不在折叠代码本身,而在公共 JS 被加载了好几遍。这篇把排查过程和背后的道理记一下。
现象
折叠按钮的代码很简单,就是切换一个 close 类:
/*
* 折叠效果:点一下,展开的收起,收起的展开
*/
$("body").on("click", ".btnFold", function () {
// 往上找两层拿到要折叠的容器
var $fat = $(this).parent().parent();
// 当前是不是展开状态(没有 close 类就是展开)
var isOpen = !$fat.hasClass("close");
// 展开就加 close 收起,收起就去掉 close 展开
var action = isOpen ? "addClass" : "removeClass";
$fat[action]("close");
});
逻辑没毛病。先怀疑是不是有人改坏了代码,走读一遍,没问题。
那问题到底在哪?
用 DEBUG 抓规律
光看代码看不出来,上断点。
在折叠方法里加个 debugger,开 Chrome 一步步跟。
结果跟出一个很有意思的规律:
- 打开第一个模块,点折叠按钮,断点进一次,正常。
- 打开第二个模块,点折叠按钮,断点进了两次,界面上看着就失效了。
- 打开第三个模块,点折叠按钮,断点进三次。
规律清楚了:打开的模块越多,这个方法被触发的次数越多。
为什么偶数次就"失效"
明白了触发次数,再想折叠为什么失效就通了。
折叠这个动作是"取反":展开变收起,收起变展开。
假设初始是收起状态,触发多次会这样连环翻:
收起(初始) → 展开(第1次) → 收起(第2次) → 展开(第3次) ...
- 触发奇数次:最终状态变了,看着是生效的。
- 触发偶数次:翻过去又翻回来,最终跟没动一样,看着就是"失效"。
所以第二个模块触发两次,一来一回抵消了,表现为按钮点了没反应。
根本不是失效,是被执行了双数次,自己把自己抵消了。
真正的原因
触发多次,说明事件被绑定了多次。事件为什么会多绑?因为绑定事件的那段公共 JS 被加载了多次。
翻一下引入方式就对上了。
公共方法 common.js 是在每个模块的 header.jsp 里引入的。
每个模块的首页 index.jsp 又各自 include 了自己的 header:
<jsp:include page="common/headerForSobest.jsp" />
这里有个前因。项目早先用的是 Iframe 框架,每个模块是独立子页面,各自加载一份 common.js,井水不犯河水,没问题。
后来做了一次框架整改,把 Iframe 换成了 jQuery.load 模式。
整改之后,所有模块其实都在同一个页面里,只是页面的某块区域用 load 换了内容。
于是每 load 进来一个模块,就把它 header 里的 common.js 又执行了一遍。$("body").on(...) 跟着多绑一次。
加载 N 个模块,折叠事件就绑了 N 次,点一下触发 N 次。谜底揭开。
怎么修
既然问题是"公共 JS 被重复加载导致事件重复绑定",修法就顺理成章:让公共 JS 只加载一次。
把 common.js 从各模块的 header 里挪出来,放到通用入口 main.jsp 里引入一次。
<!-- main.jsp:整个应用的入口,公共脚本只在这里引一次 -->
<script src="common/js/common.js"></script>
模块的 header 里不再重复引。这样不管后面 load 多少个模块,common.js 都只执行一次,事件只绑一次,折叠恢复正常。
延伸一下
这个坑的本质,是 jQuery.load 这种"局部换内容"的模式下,被 load 进来的 HTML 里如果带着 <script>,脚本会被重新执行。
除了"只加载一次"这个办法,还有几个思路可以防这类问题:
- 事件委托绑在稳定的父节点上(代码里的
$("body").on已经是委托写法),再配合解绑:绑之前先$("body").off("click", ".btnFold")一下,保证只有一份。 - 给初始化逻辑加个幂等标记,执行过就不再执行:
// 用一个全局标记保证公共初始化只跑一次
if (!window.__commonInited) {
window.__commonInited = true;
initCommonEvents(); // 真正的绑定逻辑放这里面
}
不过对这个项目来说,最干净的还是从源头把公共 JS 的引入位置收口到入口页,别让它满世界重复引。
小结
公共 JS 里的方法,加载时尽量守住"只加载一次"这条线。
一方面能躲开这种事件重复绑定、功能看着失效的怪 bug;另一方面公共文件通常不小,少加载几遍对性能也是好事。
排查这类问题的套路也值得记一下:现象不稳定、跟"打开次数"挂钩时,先怀疑是不是有东西被重复执行了,断点数一下触发次数,规律往往一下就出来了。
