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..05debc2f2 --- /dev/null +++ b/src/pages/fix.ts @@ -0,0 +1,118 @@ +// 修复 Arco Design 在 React 17+ 环境下 focusin / focusout 事件重复触发导致的 UI 卡顿问题 +// 参考 PR:https://github.com/scriptscat/scriptcat/pull/1224 +// 核心思路:将 focusin/focusout 的事件监听器执行延迟到下一个 macrotask,避免在同一渲染帧内被 Arco 多次触发 + +let actived = false; // 防止多次调用 fixArcoIssues 导致重复 patch + +export const fixArcoIssues = () => { + if (actived) return; // 已修复过则直接返回 + actived = true; + + // 保存原生的 addEventListener / removeEventListener 方法 + const originalAddEventListener = HTMLElement.prototype.addEventListener; + const originalRemoveEventListener = HTMLElement.prototype.removeEventListener; + + // 用来暂存需要延迟执行的事件对象(同一 tick 内的事件会被合并) + const stackedEvents = new Set(); + + // 记录每个事件对应的 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; + + // 使用完成后立即清理,减少 WeakMap 的引用存活时间 + bindInfoMap.delete(ev); + + // 如果事件已被 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),更稳定且调度开销更小 + self.addEventListener("message", (ev) => { + if (typeof ev.data === "object" && ev.data?.processNextTick === "addEventListenerHack") { + executorFn(); + } + }); + + // 记录原始 listener 与包装后 handler 的映射关系 + // 以便 removeEventListener 时能正确移除 + const handlerMap = new WeakMap(); + + // 自定义的 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" // 只接受 boolean 或 undefined 的 options + ) { + // 包装一层 handler,收集事件并推迟执行 + const handler = (ev: Event) => { + stackedEvents.add(ev); + bindInfoMap.set(ev, { thisArg: this, listener }); + // 发送 macrotask 讯号,让 executor 在下一个事件循环执行 + self.postMessage({ processNextTick: "addEventListenerHack" }, "*"); + }; + + // 保存原 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; + } +}; 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 );