震惊!三万star开源项目竟有致命Bug?

引子

作为前端开发,我们常常需要 借鉴一下开源项目的优秀实践

最近在用 @xyflow 实现流程图时,不巧遇到了个奇怪的问题

在本地运行时一切正常;一发上测试环境,就给你把页面搞崩了
省流:标题党,赶时间的直接看 第六小节 (=′ー`)

一、昨日重现

究竟发生了甚么事?

这功能也简单,只是实现一个流程图的切换展示而已

本地开发完成,高高兴兴就发上测试了;就在我准备提测时,页面白了,我也白了(bushi)

控制台它见红了

本地一遍遍地尝试,也复现不了

行吧,那就直接在浏览器上定位一下报错的代码

二、抽丝剥茧

首先,肯定就是直接点击错误,跳转到报错的代码

看来是这个变量 n 找不到 name 了。可我这项目代码里并没有这一行,没有操作过 removeEventListener

没关系,继续向上找

终于,一路找到了 @xyflow 的代码里

啊哈,果然是你。

写的什么垃圾,removeEventListenerdestroyon 还有 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)提出来了

顺手拿 webpackrspack 也试了一下

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 的实现也能看出来,这代码的可读性确实不敢恭维...

又水了一篇

相关推荐
掘了几秒前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅25 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法