引子
作为前端开发,我们常常需要 抄 借鉴一下开源项目的优秀实践
最近在用 @xyflow 实现流程图时,不巧遇到了个奇怪的问题
在本地运行时一切正常;一发上测试环境,就给你把页面搞崩了
省流:标题党,赶时间的直接看 第六小节 (=′ー`)
一、昨日重现
究竟发生了甚么事?
这功能也简单,只是实现一个流程图的切换展示而已

本地开发完成,高高兴兴就发上测试了;就在我准备提测时,页面白了,我也白了(bushi)
控制台它见红了
本地一遍遍地尝试,也复现不了
行吧,那就直接在浏览器上定位一下报错的代码
二、抽丝剥茧
首先,肯定就是直接点击错误,跳转到报错的代码

看来是这个变量 n
找不到 name
了。可我这项目代码里并没有这一行,没有操作过 removeEventListener
没关系,继续向上找
终于,一路找到了 @xyflow
的代码里
啊哈,果然是你。
写的什么垃圾,removeEventListener
、destroy
、on
还有 null
,这应该就是移除事件监听用的,怕不是事件类型写错了哦
我大手一挥,把它的点给去了(patch 方式修改第三方依赖,后文详述)
ts
destroy: function() {
p?.on("drag", null)
}
您猜怎么着,它真的就不报错了
三、"Fake News"
好不容易找到一个三万多 star 开源项目的 bug,给我开心坏了
我快马加鞭,写了个 pr 呈上去

结果,毫不意外地出意外了
作者给我扔来个链接,大意就是"人家文档就是这么写的,没有用错"

这直接给我干懵了
我顺着他给的链接,简单浏览了一下。好像还真的是这么用的

嗯?难道还有高手?
四、缉拿真凶
这就尴尬了,难道是我的问题?
继续查...
这里既然提到了这个 d3
,那就查它(其实控制台指向的就是这个库 -_-)
打上断点、查看库源码,最终定位到了这个函数上

嗯?removeEventListener
!很像啊
到这里,其实已经破案了,凶手就是 d3-selection
通过断点,找到构建后的代码,onRemove 方法成了下面这个样子 👇
ts
function r(t) {
return function () {
var e = this.__on;
if (e) {
for (var n, r = 0, o = -1, i = e.length; r < i; ++r)
(!t.type || (n = e[r]).type === t.type) && n.name === t.name
? this.removeEventListener(n.type, n.listener, n.options)
: (e[++o] = n);
++o ? (e.length = o) : delete this.__on;
}
};
}
这里其实就能很明显地看出来了,这个判断语句是有问题的
ts
(!t.type || (n = e[r]).type === t.type) && n.name === t.name;
当 !t.type
为 true 时,不会执行 (n = e[r]).type === t.type
这部分表达式。n 自然就还是 undefined
这是由逻辑或 ||
操作符的 短路求值 特性所决定的
五、药到病除
既然找到原因了,接下来就是如何修复了
最理想的方式肯定是提交个 pr ,直接修复这个库
但这个方式太慢了,我这里打算用 pnpm 的 patch 命令,直接修改第三方依赖
说干就干,在应用根目录执行 pnpm patch d3-selection
pnpm 自动生成一份 d3-selection 的临时文件,进到临时文件中,修改 onRemove 函数
ts
function onRemove(typename) {
return function () {
var on = this.__on;
if (!on) return;
for (var j = 0, i = -1, m = on.length; j < m; ++j) {
var o = on[j]; // 提出 o 变量
if (
(!typename.type || o.type === typename.type) &&
o.name === typename.name
) {
this.removeEventListener(o.type, o.listener, o.options);
} else {
on[++i] = o;
}
}
if (++i) on.length = i;
else delete this.__on;
};
}
修改完成,保存一下。
执行一下 pnpm patch-commit xxx
,其中 xxx
就是刚刚生成的临时文件地址
同时,在项目根目录下,自动生成了 patches 文件夹
patch
diff --git a/src/selection/on.js b/src/selection/on.js
index 7906c8ca2c3683a2c4e99e638d38d2bbd2b32752..9fa1911e456d1a6d3f962cb77832c094f5ec6c61 100644
--- a/src/selection/on.js
+++ b/src/selection/on.js
@@ -16,8 +16,9 @@ function onRemove(typename) {
return function() {
var on = this.__on;
if (!on) return;
- for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
- if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
+ for (var j = 0, i = -1, m = on.length; j < m; ++j) {
+ var o = on[j];
+ if ((!typename.type || o.type === typename.type) && o.name === typename.name) {
this.removeEventListener(o.type, o.listener, o.options);
} else {
on[++i] = o;
package.json
文件中也自动生成了一条配置

到这里这个问题就算是解决了
六、追根溯源
你以为这就结束了吗?肉、肉、肉
修复好了以后,我想着也顺便提个 pr 给 d3-selection
吧
就顺势撇了一眼 d3-selection
的 issue,发现我这个问题没有人出现过;再看一眼最近提交时间,最近一次提交已经是两年前了
这就让人很疑惑了,这个 bug 应该很明显啊?我这一构建就出现了
等等,构建
难道....
抱着试一试的心态,我用 vite 新建了个应用,同样实现了一遍 @xyflow
的流程图功能
开始构建
ts
// onRemove 经过 vite(rollup)构建
function a7(e) {
return function () {
var t = this.__on;
if (t) {
for (var n = 0, r = -1, i = t.length, u; n < i; ++n)
(u = t[n]),
(!e.type || u.type === e.type) && u.name === e.name
? this.removeEventListener(u.type, u.listener, u.options)
: (t[++r] = u);
++r ? (t.length = r) : delete this.__on;
}
};
}
好好好,这回的产物居然没问题,自动把变量(u)提出来了
顺手拿 webpack
和 rspack
也试了一下
ts
function es(e) {
return function () {
var t = this.__on;
if (t) {
for (var n, o = 0, r = -1, i = t.length; o < i; ++o)
((n = t[o]), (e.type && n.type !== e.type) || n.name !== e.name)
? (t[++r] = n)
: this.removeEventListener(n.type, n.listener, n.options);
++r ? (t.length = r) : delete this.__on;
}
};
}
onRemove 经过这俩的构建产物几乎是一样的;同样没有问题
现在答案就很明显了,就是我项目里用的 构建工具 的锅
这里使用的是公司自研的构建工具,但底层是 0.4
版本的 rspack
,顺着这个查了一下,发现0.4.8
之前的版本都有这个问题
那只能摇人了,让同事升级一下版本吧 ~
后记
这个 bug 算是比较有意思的,短路求值
+ 压缩优化
才诞生了这个藏的极深的 bug。没想到构建后与原代码还会出现执行不一致的情况;不过从 onRemove 的实现也能看出来,这代码的可读性确实不敢恭维...
又水了一篇