From bc3b55abe383ea4184035afa423e2b73d6823e24 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:26:00 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20ArcoDesign/React=20?= =?UTF-8?q?=E7=9A=84=20focusin/out=20=E4=BA=8B=E4=BB=B6=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/batchupdate/main.tsx | 3 +++ src/pages/confirm/main.tsx | 3 +++ src/pages/fix.ts | 48 ++++++++++++++++++++++++++++++++++ src/pages/import/main.tsx | 3 +++ src/pages/install/main.tsx | 3 +++ src/pages/options/main.tsx | 3 +++ src/pages/popup/main.tsx | 3 +++ 7 files changed, 66 insertions(+) create mode 100644 src/pages/fix.ts diff --git a/src/pages/batchupdate/main.tsx b/src/pages/batchupdate/main.tsx index f87ac8a8a..59d7c7647 100644 --- a/src/pages/batchupdate/main.tsx +++ b/src/pages/batchupdate/main.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; @@ -27,6 +28,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); diff --git a/src/pages/confirm/main.tsx b/src/pages/confirm/main.tsx index 51985671b..14d20dde8 100644 --- a/src/pages/confirm/main.tsx +++ b/src/pages/confirm/main.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; @@ -26,6 +27,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); diff --git a/src/pages/fix.ts b/src/pages/fix.ts new file mode 100644 index 000000000..1720fbe00 --- /dev/null +++ b/src/pages/fix.ts @@ -0,0 +1,48 @@ +let actived = false; + +export const fixArcoIssues = () => { + if (actived) return; + actived = true; + + const originalAddEventListener = HTMLElement.prototype.addEventListener; + type BindInfo = { thisArg: Element; listener: EventListener }; + + const stackedEvents = new Set(); + const bindInfoMap = new WeakMap(); + const executorFn = () => { + const events = [...stackedEvents]; + stackedEvents.clear(); + for (const ev of events) { + if (ev.defaultPrevented) continue; + const bi = bindInfoMap.get(ev); + if (!bi) continue; + bindInfoMap.delete(ev); + try { + bi.listener.call(bi.thisArg, ev); + } catch (err) { + console.error(err); + } + } + }; + + const addEventListenerHack = function ( + this: Element, + type: K, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + if ((type === "focusin" || type === "focusout") && typeof listener === "function") { + const handler = (event: Event) => { + stackedEvents.add(event); + bindInfoMap.set(event, { thisArg: this, listener }); + requestAnimationFrame(executorFn); + }; + return originalAddEventListener.call(this, type, handler, options); + } + return originalAddEventListener.call(this, type, listener, options); + }; + document.body.addEventListener = addEventListenerHack; + + const root = document.querySelector("div#root"); + if (root) root.addEventListener = addEventListenerHack; +}; diff --git a/src/pages/import/main.tsx b/src/pages/import/main.tsx index fe51ddedd..2802dd082 100644 --- a/src/pages/import/main.tsx +++ b/src/pages/import/main.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; @@ -26,6 +27,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); diff --git a/src/pages/install/main.tsx b/src/pages/install/main.tsx index 7f87023e2..95f4faf2f 100644 --- a/src/pages/install/main.tsx +++ b/src/pages/install/main.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; import MainLayout from "../components/layout/MainLayout.tsx"; import LoggerCore from "@App/app/logger/core.ts"; import { message } from "../store/global.ts"; @@ -38,6 +39,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); diff --git a/src/pages/options/main.tsx b/src/pages/options/main.tsx index 096203c58..bbd927b77 100644 --- a/src/pages/options/main.tsx +++ b/src/pages/options/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client"; import MainLayout from "../components/layout/MainLayout.tsx"; import Sider from "../components/layout/Sider.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; import "@arco-design/web-react/dist/css/arco.css"; import "@App/locales/locales"; import "@App/index.css"; @@ -33,6 +34,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); diff --git a/src/pages/popup/main.tsx b/src/pages/popup/main.tsx index 05b3e94c1..d0b21556c 100644 --- a/src/pages/popup/main.tsx +++ b/src/pages/popup/main.tsx @@ -10,6 +10,7 @@ import "@App/index.css"; import "./index.css"; import PopupLayout from "../components/layout/PopupLayout.tsx"; import { AppProvider } from "../store/AppContext.tsx"; +import { fixArcoIssues } from "@App/pages/fix.ts"; // 初始化日志组件 const loggerCore = new LoggerCore({ @@ -27,6 +28,8 @@ const Root = ( ); +fixArcoIssues(); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( process.env.NODE_ENV === "development" ? {Root} : Root ); From 53d561d12a8beeceea53154a628e7254627757c9 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:32:37 +0900 Subject: [PATCH 2/9] Update fix.ts --- src/pages/fix.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 1720fbe00..c27530616 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -10,6 +10,7 @@ export const fixArcoIssues = () => { const stackedEvents = new Set(); const bindInfoMap = new WeakMap(); const executorFn = () => { + if (!stackedEvents.size) return; const events = [...stackedEvents]; stackedEvents.clear(); for (const ev of events) { From 0a4b69a0b0fb553d0483c9b5d032a174cf0b515d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:44:43 +0900 Subject: [PATCH 3/9] Update src/pages/fix.ts Co-authored-by: wangyizhi --- src/pages/fix.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index c27530616..9afbfb9a3 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -1,3 +1,5 @@ +// 修复arco中的事件问题 https://github.com/scriptscat/scriptcat/pull/1224/ + let actived = false; export const fixArcoIssues = () => { From 025411a63e09b89f5fd6b90eb5e268958b7d886a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:52:46 +0900 Subject: [PATCH 4/9] fix code order --- src/pages/fix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 9afbfb9a3..63b62a0af 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -16,10 +16,10 @@ export const fixArcoIssues = () => { const events = [...stackedEvents]; stackedEvents.clear(); for (const ev of events) { - if (ev.defaultPrevented) continue; const bi = bindInfoMap.get(ev); if (!bi) continue; bindInfoMap.delete(ev); + if (ev.defaultPrevented) continue; try { bi.listener.call(bi.thisArg, ev); } catch (err) { From 42a6ca355b744961025be97d9804a7f5f19b5471 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:32:32 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=E6=8A=8A=E4=BA=8B=E4=BB=B6=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E6=8E=92=E7=A8=8B=E5=9C=A8=E4=B8=8B=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=20marcoTask=20=E8=80=8C=E9=9D=9E=20rAF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/fix.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 63b62a0af..612877d57 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -28,6 +28,12 @@ export const fixArcoIssues = () => { } }; + self.addEventListener("message", (ev) => { + if (typeof ev.data === "object" && ev.data?.browserNextTick === "addEventListenerHack") { + executorFn(); + } + }); + const addEventListenerHack = function ( this: Element, type: K, @@ -35,10 +41,10 @@ export const fixArcoIssues = () => { options?: boolean | AddEventListenerOptions ): void { if ((type === "focusin" || type === "focusout") && typeof listener === "function") { - const handler = (event: Event) => { - stackedEvents.add(event); - bindInfoMap.set(event, { thisArg: this, listener }); - requestAnimationFrame(executorFn); + const handler = (ev: Event) => { + stackedEvents.add(ev); + bindInfoMap.set(ev, { thisArg: this, listener }); + self.postMessage({ browserNextTick: "addEventListenerHack" }); }; return originalAddEventListener.call(this, type, handler, options); } From 76958f47ecdaf459b86c78f16b6c9877a030ef24 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:41:36 +0900 Subject: [PATCH 6/9] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20options=20=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/fix.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 612877d57..38bf0549f 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -40,7 +40,11 @@ export const fixArcoIssues = () => { listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions ): void { - if ((type === "focusin" || type === "focusout") && typeof listener === "function") { + if ( + (type === "focusin" || type === "focusout") && + typeof listener === "function" && + typeof (options ?? false) === "boolean" // accept capture event or bubble event but exclude the advanced options like "once" + ) { const handler = (ev: Event) => { stackedEvents.add(ev); bindInfoMap.set(ev, { thisArg: this, listener }); From d915d80f59ee363a79b9e7a78cf75433b643a250 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:05:06 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=E5=8A=A0=E5=85=A5AI=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/fix.ts | 50 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 38bf0549f..74b82c8a3 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -1,61 +1,91 @@ -// 修复arco中的事件问题 https://github.com/scriptscat/scriptcat/pull/1224/ +// 修复 Arco Design 在 React 17+ 环境下 focusin / focusout 事件重复触发导致的 UI 卡顿问题 +// 参考 PR:https://github.com/scriptscat/scriptcat/pull/1224 +// 核心思路:将 focusin/focusout 的事件监听器执行延迟到下一个 macrotask,避免在同一渲染帧内被 Arco 多次触发 -let actived = false; +let actived = false; // 防止多次调用 fixArcoIssues 导致重复 patch export const fixArcoIssues = () => { - if (actived) return; + if (actived) return; // 已修复过则直接返回 actived = true; + // 保存原生的 addEventListener 方法 const originalAddEventListener = HTMLElement.prototype.addEventListener; - type BindInfo = { thisArg: Element; listener: EventListener }; + // 用来暂存需要延迟执行的事件物件 const stackedEvents = new Set(); - const bindInfoMap = new WeakMap(); + + // 记录每个事件对应的 thisArg 和 listener(因为我们会包一层 handler) + const bindInfoMap = new WeakMap(); + + // 真正执行被延迟的事件回调 const executorFn = () => { if (!stackedEvents.size) return; + + // 复制一份后清空,避免在执行期间又有新事件进来 const events = [...stackedEvents]; stackedEvents.clear(); + for (const ev of events) { const bi = bindInfoMap.get(ev); if (!bi) continue; - bindInfoMap.delete(ev); + + bindInfoMap.delete(ev); // 用完即清理,减少 WeakMap 引用 + + // 如果事件已被 preventDefault,则不再执行原回调(保持标准行为) if (ev.defaultPrevented) continue; + try { + // 使用原来的 this 和 listener 执行 bi.listener.call(bi.thisArg, ev); } catch (err) { - console.error(err); + console.error("Failed to execute delayed callback.", err); } } }; + // 使用 postMessage + message 事件来实现 macrotask(比 setTimeout(0) 更可靠且开销较小) self.addEventListener("message", (ev) => { if (typeof ev.data === "object" && ev.data?.browserNextTick === "addEventListenerHack") { executorFn(); } }); + // 自订的 addEventListener 拦截器,只针对 focusin/focusout 且 options 为简单 boolean 时生效 const addEventListenerHack = function ( this: Element, type: K, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions ): void { + // 只拦截 focusin / focusout,且 listener 是函数,且 options 是简单的 capture/bubble 设定 + // (排除 once、passive 等进阶选项,避免破坏其他使用方式) if ( (type === "focusin" || type === "focusout") && typeof listener === "function" && - typeof (options ?? false) === "boolean" // accept capture event or bubble event but exclude the advanced options like "once" + typeof (options ?? false) === "boolean" // 只接受 boolean 或 undefined 的 options ) { + // 包装一层 handler,收集事件并推迟执行 const handler = (ev: Event) => { stackedEvents.add(ev); bindInfoMap.set(ev, { thisArg: this, listener }); - self.postMessage({ browserNextTick: "addEventListenerHack" }); + // 发送 macrotask 讯号,让 executor 在下一个事件循环执行 + self.postMessage({ browserNextTick: "addEventListenerHack" }, "*"); }; + + // 用包装后的 handler 注册真正的事件 return originalAddEventListener.call(this, type, handler, options); } + + // 其他事件走原生方法,不做干预 return originalAddEventListener.call(this, type, listener, options); }; + + // 针对 body 打补丁(Arco 大量事件绑在 document 或 body 上) document.body.addEventListener = addEventListenerHack; + // 也针对 React 根节点 #root 打补丁(部分组件可能绑在根元素) const root = document.querySelector("div#root"); - if (root) root.addEventListener = addEventListenerHack; + if (root) { + root.addEventListener = addEventListenerHack; + } }; From f787cfd4e67561f9e94a840d198ac71ca8abe830 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:12:43 +0900 Subject: [PATCH 8/9] browserNextTick -> processNextTick --- src/pages/fix.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index 74b82c8a3..f3424a43d 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -45,7 +45,7 @@ export const fixArcoIssues = () => { // 使用 postMessage + message 事件来实现 macrotask(比 setTimeout(0) 更可靠且开销较小) self.addEventListener("message", (ev) => { - if (typeof ev.data === "object" && ev.data?.browserNextTick === "addEventListenerHack") { + if (typeof ev.data === "object" && ev.data?.processNextTick === "addEventListenerHack") { executorFn(); } }); @@ -69,7 +69,7 @@ export const fixArcoIssues = () => { stackedEvents.add(ev); bindInfoMap.set(ev, { thisArg: this, listener }); // 发送 macrotask 讯号,让 executor 在下一个事件循环执行 - self.postMessage({ browserNextTick: "addEventListenerHack" }, "*"); + self.postMessage({ processNextTick: "addEventListenerHack" }, "*"); }; // 用包装后的 handler 注册真正的事件 From e61ed4ff56becd9d3b2bd6a03b944daed4b9454f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:41:19 +0900 Subject: [PATCH 9/9] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20removeEventListenerHac?= =?UTF-8?q?k=EF=BC=8C=20=E4=BF=AE=E8=AE=A2=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/fix.ts | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/pages/fix.ts b/src/pages/fix.ts index f3424a43d..05debc2f2 100644 --- a/src/pages/fix.ts +++ b/src/pages/fix.ts @@ -8,10 +8,11 @@ export const fixArcoIssues = () => { if (actived) return; // 已修复过则直接返回 actived = true; - // 保存原生的 addEventListener 方法 + // 保存原生的 addEventListener / removeEventListener 方法 const originalAddEventListener = HTMLElement.prototype.addEventListener; + const originalRemoveEventListener = HTMLElement.prototype.removeEventListener; - // 用来暂存需要延迟执行的事件物件 + // 用来暂存需要延迟执行的事件对象(同一 tick 内的事件会被合并) const stackedEvents = new Set(); // 记录每个事件对应的 thisArg 和 listener(因为我们会包一层 handler) @@ -29,28 +30,37 @@ export const fixArcoIssues = () => { const bi = bindInfoMap.get(ev); if (!bi) continue; - bindInfoMap.delete(ev); // 用完即清理,减少 WeakMap 引用 + // 使用完成后立即清理,减少 WeakMap 的引用存活时间 + bindInfoMap.delete(ev); - // 如果事件已被 preventDefault,则不再执行原回调(保持标准行为) + // 如果事件已被 preventDefault,则不再执行原回调 + // 保持浏览器原生事件行为一致 if (ev.defaultPrevented) continue; try { // 使用原来的 this 和 listener 执行 bi.listener.call(bi.thisArg, ev); } catch (err) { + // 捕获异常,避免影响后续事件执行 console.error("Failed to execute delayed callback.", err); } } }; - // 使用 postMessage + message 事件来实现 macrotask(比 setTimeout(0) 更可靠且开销较小) + // 使用 postMessage + message 事件来模拟 macrotask + // 相比 setTimeout(0),更稳定且调度开销更小 self.addEventListener("message", (ev) => { if (typeof ev.data === "object" && ev.data?.processNextTick === "addEventListenerHack") { executorFn(); } }); - // 自订的 addEventListener 拦截器,只针对 focusin/focusout 且 options 为简单 boolean 时生效 + // 记录原始 listener 与包装后 handler 的映射关系 + // 以便 removeEventListener 时能正确移除 + const handlerMap = new WeakMap(); + + // 自定义的 addEventListener + // 只针对 focusin / focusout 且 options 为简单 boolean 的情况生效 const addEventListenerHack = function ( this: Element, type: K, @@ -72,20 +82,37 @@ export const fixArcoIssues = () => { self.postMessage({ processNextTick: "addEventListenerHack" }, "*"); }; - // 用包装后的 handler 注册真正的事件 + // 保存原 listener 与包装 handler 的对应关系 + handlerMap.set(listener, handler); + + // 实际注册的是包装后的 handler return originalAddEventListener.call(this, type, handler, options); } - // 其他事件走原生方法,不做干预 + // 其他事件保持原生行为,不做任何干预 return originalAddEventListener.call(this, type, listener, options); }; + // 自定义的 removeEventListener + // 如果 listener 曾被包装过,这里需要移除对应的 handler + const removeEventListenerHack = function ( + this: Element, + type: K, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void { + const handler = typeof listener === "function" && handlerMap.get(listener); + return originalRemoveEventListener.call(this, type, handler || listener, options); + }; + // 针对 body 打补丁(Arco 大量事件绑在 document 或 body 上) document.body.addEventListener = addEventListenerHack; + document.body.removeEventListener = removeEventListenerHack; // 也针对 React 根节点 #root 打补丁(部分组件可能绑在根元素) const root = document.querySelector("div#root"); if (root) { root.addEventListener = addEventListenerHack; + root.removeEventListener = removeEventListenerHack; } };