React 错误 321:Semi 与 antd 各自加载 React 执行两遍,导致两套 React #73

Open
opened 2026-11-05 09:38:46 +00:00 by laoyang · 0 comments
Owner

React #321:Semi 与 antd 各自加载 React,执行两遍导致两套 React

用户(2026-11-05)Console 输出

AntD 日期控件已挂上 5 处        ← 首屏
AntD 日期控件已挂上 1 处        ← 点开任务浮层的编辑
react-dom.min.js:121 Error: Minified React error #321   ← Invalid hook call
    at Object.V (VM320 react-dom.min.js:105:148)
    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
    bootAntd @ app.js:1957
    window.fbBootAntd @ app.js:2501
    loadEdit @ entry_popup.js:280

React #321 = Invalid hook call

官方三种成因:① React 版本不一致 ② 存在多份 React 副本 ③ 在组件外调 hook。

这里的是 ②。

决定性证据:VM 前缀

VM320 react-dom.min.js
VM303 react.min.js

VM<数字> 是浏览器给"通过 eval / new Function 动态执行的代码"分配的脚本 ID。
普通的 <script src> 加载显示的是文件 URL(如 react-dom.min.js?v=6735aef8)。

同一个文件出现 VM 前缀 ⇒ 它被动态执行了不止一次。

根因:两个 loadJs 都不去重

app.js 里有两个独立的 IIFE,各自定义了一个 loadJs:

Semi 段(约 1600 行):

function loadJs(src) {
  return new Promise(function (res, rej) {
    var s = document.createElement('script');
    s.src = src;
    s.onload = ...
  });
}

boot() 里无条件调用:

loadJs(conf.react)
  .then(function () { return loadJs(conf.dom); })
  .then(function () { return loadJs(conf.ui); })

antd 段(约 93 行):

if (!window.React) { chain = chain.then(function () { return loadJs(conf.react || ''); }); }

看着更谨慎,但依赖"当前全局有没有 React" —— 而 UMD 要执行完才赋值。

时序

① 首页加载,Semi 的 boot() 用 requestIdleCallback 排到"空闲时"
② 用户点开任务 → 浮层 → 编辑 → fbBootAntd()
③ antd 的 ensure():!window.React ⇒ 开始加载 react.min.js  ← 第一次
④ 与此同时/稍后,Semi 的 idle 回调触发 → boot() → 无条件 loadJs(react.min.js)  ← 第二次
⑤ 同一段 UMD 执行两次 ⇒ window.React 被覆盖成"新的那套"
⑥ 但 antd 内部模块早已捕获"第一次"的 React 引用
⑦ antd 的 useContext 在"第二套"的 dispatcher 上调用 ⇒ #321

ensure() 有个致命位置问题:

if (window.antd && window.dayjs && window.React && window.ReactDOM) {
    return Promise.resolve();
}

React 已就绪时会直接 return —— 这正是首屏 "已挂上 5 处" 之后不再加载的原因,
但也意味着首次加载时两边真的会各插一个 <script>。

修法:按 src 全局去重

两处 loadJs 都改成共享一个全局登记表:

function loadJs(src) {
  var reg = window.__fbJsLoaded;
  if (!reg) { reg = window.__fbJsLoaded = {}; }
  if (reg[src]) { return reg[src]; }        // 已加载/加载中 ⇒ 复用同一个 Promise
  reg[src] = new Promise(function (res, rej) { ... });
  return reg[src];
}

为什么用 window.__fbJsLoaded 而不是各 IIFE 内的变量:
两个 loadJs 在不同的 IIFE 闭包里,各自的局部变量互相看不见。

为什么不用 if (!window.React) 那种判断:
UMD 执行完才赋值。两个请求几乎同时发出时,两边都还看不到 window.React,
照样会各插一个。按 URL 去重是确定的,不依赖执行时序。

验证(复刻逻辑跑)

=== antd 先,Semi 的 idle 后到 ===
  antd -> 新插入 ← 真实执行
  semi -> 复用 ✅ 不重复执行
  结果: ✅ 只有一份 React

=== Semi 先,antd 后 ===
  semi -> 新插入 | antd -> 复用 ✅
  结果: ✅ 只有一份 React

=== 不同文件应各自加载 ===
  react.min.js -> 新插入 ✅
  antd.min.js  -> 新插入 ✅(没被错误复用)

验收

  • 首页点任务 → 编辑 → Console 不再出现 React #321
  • 「截止时间」的 AntD 选择器正常显示,能点开选日期
  • Console 仍能看到 AntD 日期控件已挂上 N 处
  • 观影模块的日期控件不受影响(Semi 那套)

为什么单独建单

与 #72(任务浮层缺 ref_id 导致编辑按钮不显示)是两件事:#72 是按钮压根没出现;本单是按钮出现后、点编辑时 AntD 控件因 React 重复加载而崩溃。修完 #72 才暴露出来的下一个 bug。

# React #321:Semi 与 antd 各自加载 React,执行两遍导致两套 React ## 用户(2026-11-05)Console 输出 ``` AntD 日期控件已挂上 5 处 ← 首屏 AntD 日期控件已挂上 1 处 ← 点开任务浮层的编辑 react-dom.min.js:121 Error: Minified React error #321 ← Invalid hook call at Object.V (VM320 react-dom.min.js:105:148) 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 bootAntd @ app.js:1957 window.fbBootAntd @ app.js:2501 loadEdit @ entry_popup.js:280 ``` ## React #321 = Invalid hook call 官方三种成因:① React 版本不一致 ② **存在多份 React 副本** ③ 在组件外调 hook。 这里的是 **②**。 ## 决定性证据:`VM` 前缀 ``` VM320 react-dom.min.js VM303 react.min.js ``` **`VM<数字>` 是浏览器给"通过 eval / new Function 动态执行的代码"分配的脚本 ID。** 普通的 `<script src>` 加载显示的是文件 URL(如 `react-dom.min.js?v=6735aef8`)。 同一个文件出现 `VM` 前缀 ⇒ **它被动态执行了不止一次**。 ## 根因:两个 `loadJs` 都不去重 `app.js` 里有**两个独立的 IIFE**,各自定义了一个 `loadJs`: **Semi 段(约 1600 行)**: ```javascript function loadJs(src) { return new Promise(function (res, rej) { var s = document.createElement('script'); s.src = src; s.onload = ... }); } ``` `boot()` 里**无条件**调用: ```javascript loadJs(conf.react) .then(function () { return loadJs(conf.dom); }) .then(function () { return loadJs(conf.ui); }) ``` **antd 段(约 93 行)**: ```javascript if (!window.React) { chain = chain.then(function () { return loadJs(conf.react || ''); }); } ``` 看着更谨慎,但**依赖"当前全局有没有 React"** —— 而 UMD 要执行完才赋值。 ### 时序 ``` ① 首页加载,Semi 的 boot() 用 requestIdleCallback 排到"空闲时" ② 用户点开任务 → 浮层 → 编辑 → fbBootAntd() ③ antd 的 ensure():!window.React ⇒ 开始加载 react.min.js ← 第一次 ④ 与此同时/稍后,Semi 的 idle 回调触发 → boot() → 无条件 loadJs(react.min.js) ← 第二次 ⑤ 同一段 UMD 执行两次 ⇒ window.React 被覆盖成"新的那套" ⑥ 但 antd 内部模块早已捕获"第一次"的 React 引用 ⑦ antd 的 useContext 在"第二套"的 dispatcher 上调用 ⇒ #321 ``` `ensure()` 有个致命位置问题: ```javascript if (window.antd && window.dayjs && window.React && window.ReactDOM) { return Promise.resolve(); } ``` React 已就绪时会直接 return —— **这正是首屏 "已挂上 5 处" 之后不再加载的原因**, 但也意味着首次加载时两边真的会各插一个 `<script>`。 ## 修法:按 `src` 全局去重 两处 `loadJs` 都改成共享一个全局登记表: ```javascript function loadJs(src) { var reg = window.__fbJsLoaded; if (!reg) { reg = window.__fbJsLoaded = {}; } if (reg[src]) { return reg[src]; } // 已加载/加载中 ⇒ 复用同一个 Promise reg[src] = new Promise(function (res, rej) { ... }); return reg[src]; } ``` **为什么用 `window.__fbJsLoaded` 而不是各 IIFE 内的变量**: 两个 `loadJs` 在**不同的 IIFE 闭包**里,各自的局部变量互相看不见。 **为什么不用 `if (!window.React)` 那种判断**: UMD 执行完才赋值。两个请求几乎同时发出时,两边都还看不到 `window.React`, 照样会各插一个。**按 URL 去重是确定的,不依赖执行时序。** ## 验证(复刻逻辑跑) ``` === antd 先,Semi 的 idle 后到 === antd -> 新插入 ← 真实执行 semi -> 复用 ✅ 不重复执行 结果: ✅ 只有一份 React === Semi 先,antd 后 === semi -> 新插入 | antd -> 复用 ✅ 结果: ✅ 只有一份 React === 不同文件应各自加载 === react.min.js -> 新插入 ✅ antd.min.js -> 新插入 ✅(没被错误复用) ``` ## 验收 - [ ] 首页点任务 → 编辑 → Console **不再出现 React #321** - [ ] 「截止时间」的 AntD 选择器正常显示,能点开选日期 - [ ] Console 仍能看到 `AntD 日期控件已挂上 N 处` - [ ] 观影模块的日期控件不受影响(Semi 那套) --- ### 为什么单独建单 与 #72(任务浮层缺 ref_id 导致编辑按钮不显示)是两件事:#72 是按钮压根没出现;本单是按钮出现后、点编辑时 AntD 控件因 React 重复加载而崩溃。修完 #72 才暴露出来的下一个 bug。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#73