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

又水了一篇

相关推荐
GISer_Jing4 小时前
前端实习总结——案例与大纲
前端·javascript
天天进步20154 小时前
前端工程化:Webpack从入门到精通
前端·webpack·node.js
姑苏洛言5 小时前
编写产品需求文档:黄历日历小程序
前端·javascript·后端
知识分享小能手6 小时前
Vue3 学习教程,从入门到精通,使用 VSCode 开发 Vue3 的详细指南(3)
前端·javascript·vue.js·学习·前端框架·vue·vue3
姑苏洛言6 小时前
搭建一款结合传统黄历功能的日历小程序
前端·javascript·后端
hackchen6 小时前
Go与JS无缝协作:Goja引擎实战之错误处理最佳实践
开发语言·javascript·golang
你的人类朋友7 小时前
🤔什么时候用BFF架构?
前端·javascript·后端
知识分享小能手7 小时前
Bootstrap 5学习教程,从入门到精通,Bootstrap 5 表单验证语法知识点及案例代码(34)
前端·javascript·学习·typescript·bootstrap·html·css3