修 React #321:loadJs 按 src 全局去重,避免 React 执行两遍 (#73)
用户 Console 决定性证据: AntD 日期控件已挂上 5 处 ← 首屏 AntD 日期控件已挂上 1 处 ← 点开浮层编辑 react-dom.min.js:121 Error: Minified React error #321 (Invalid hook call) at c.useContext (VM303 react.min.js:28:406) at e (antd-with-locales.min.js:77:5889) renderPicker @ app.js:2279 / mountAntdSingleDT @ app.js:2282 #321 = Invalid hook call,本次成因是"存在多份 React 副本"。 决定性线索是 VM 前缀:VM303 / VM320。 那是浏览器给 eval/new Function 动态执行的代码分配的脚本 ID; 普通 <script src> 显示的是文件 URL。同一个文件出现 VM 前缀 ⇒ 它被动态执行了不止一次。 根因:app.js 里有两个独立 IIFE,各自一个 loadJs,都不去重。 · Semi 段 boot():**无条件** loadJs(conf.react) + loadJs(conf.dom) · antd 段 ensure():只在 !window.React 时才加载 —— 看着谨慎, 但依赖"当前全局有没有 React",而 UMD 执行完才赋值。 时序:Semi 的 boot() 排在 requestIdleCallback 里。 用户点开任务 → fbBootAntd() → antd 开始加载 react.min.js(第一次) → Semi 的 idle 回调恰好触发 → 又插一遍(第二次) → 同一段 UMD 执行两次 → window.React 被覆盖 → 但 antd 内部已捕获第一次的 React 引用 → antd 的 useContext 在第二套的 dispatcher 上调用 → #321 修法:两个 loadJs 都改为按 src 全局去重,共享 window.__fbJsLoaded。 · 用全局而不是 IIFE 局部变量:两个 loadJs 在不同闭包里,互相看不见 · 不用 if (!window.React) 那种判断:UMD 执行完才赋值, 两个请求几乎同时发出时两边都还看不到 window.React,照样各插一个。 按 URL 去重是确定的,不依赖时序。 验证(复刻逻辑跑了三种时序): antd 先 / Semi 后 → 第二个复用 ✅ 只有一份 Semi 先 / antd 后 → 第二个复用 ✅ 只有一份 不同文件 → 各自加载 ✅ 没被错误复用
This commit is contained in:
+42
-2
@@ -90,14 +90,21 @@
|
||||
document.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
/* ⚠️ 与 Semi 段那个 `loadJs` 用**同一个全局登记表** —— 两处都加载 React
|
||||
(Semi 无条件、antd 有条件),不共享去重就会执行两次 ⇒ React #321。
|
||||
详见 Semi 段 `loadJs` 上方的完整说明。 */
|
||||
function loadJs(src) {
|
||||
return new Promise(function (res, rej) {
|
||||
var reg = window.__fbJsLoaded;
|
||||
if (!reg) { reg = window.__fbJsLoaded = {}; }
|
||||
if (reg[src]) { return reg[src]; }
|
||||
reg[src] = new Promise(function (res, rej) {
|
||||
var el = document.createElement('script');
|
||||
el.src = src;
|
||||
el.onload = function () { res(); };
|
||||
el.onerror = function () { rej(new Error('加载失败: ' + src)); };
|
||||
document.head.appendChild(el);
|
||||
});
|
||||
return reg[src];
|
||||
}
|
||||
function ensureCropper() {
|
||||
if (window.Cropper) { return Promise.resolve(); }
|
||||
@@ -1597,14 +1604,47 @@
|
||||
document.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 加载一个 JS 文件。
|
||||
*
|
||||
* 🔴🔴 2026-11-05 修 React #321(Invalid hook call):
|
||||
*
|
||||
* 症状:首页点任务 → 浮层点「编辑」→ 控制台的 `AntD 日期控件已挂上 1 处`
|
||||
* 紧接着抛 `Minified React error #321`,日期控件崩掉。
|
||||
*
|
||||
* 根因:**React 被加载了两遍**。
|
||||
* · Semi 段的 `boot()` 用 `requestIdleCallback` 延迟跑,
|
||||
* 里面**无条件** `loadJs(conf.react)` + `loadJs(conf.dom)`;
|
||||
* · antd 段的 `ensure()` 只在 `!window.React` 时才加载 —— 看着更"谨慎",
|
||||
* 但两边谁先谁后取决于 idle 回调的时机。
|
||||
* ⇒ antd 先加载、Semi 的 idle 后到 ⇒ Semi **又插了一遍**同一个
|
||||
* `react.min.js` ⇒ 那段 UMD **执行第二次** ⇒ `window.React` 被覆盖。
|
||||
* 而 antd 已经在内部捕获了**第一次**的 React 引用 ⇒ 两套 React 并存
|
||||
* ⇒ antd 的 hook(useContext)在"第二套"的 dispatcher 上调用 ⇒ #321。
|
||||
*
|
||||
* 堆栈里 `VM303 react.min.js` / `VM320 react-dom.min.js` 这种 **VM 前缀**
|
||||
* 就是"同一个文件被 eval 执行了不止一次"的铁证。
|
||||
*
|
||||
* ⇒ 修法:按 `src` **全局去重**,同一个地址永远只执行一次。
|
||||
* 缓存在 `window.__fbJsLoaded`(跨 IIFE 共享 —— 本文件里有两个独立的
|
||||
* `loadJs` 定义,Semi 一个、Semi 外层一个,各自闭包内的变量互相看不见)。
|
||||
*
|
||||
* ⚠️ 不能只给 Semi 那边加判断(`if (!window.React)`):那种写法依赖"现有全局
|
||||
* 是否已经有 React",而 UMD 执行完才赋值 —— 两个请求几乎同时发出时,
|
||||
* 两边都还看不到 `window.React`,照样会各插一个。**按 URL 去重才是确定的。**
|
||||
*/
|
||||
function loadJs(src) {
|
||||
return new Promise(function (res, rej) {
|
||||
var reg = window.__fbJsLoaded;
|
||||
if (!reg) { reg = window.__fbJsLoaded = {}; }
|
||||
if (reg[src]) { return reg[src]; }
|
||||
reg[src] = new Promise(function (res, rej) {
|
||||
var s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = function () { res(); };
|
||||
s.onerror = function () { rej(new Error('加载失败: ' + src)); };
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return reg[src];
|
||||
}
|
||||
|
||||
function pad(n) { return (n < 10 ? '0' : '') + n; }
|
||||
|
||||
Reference in New Issue
Block a user