跳转至

Deep Dive | src/screens/REPL.tsx 5005 行 — Claude Code 主屏幕(God Component)

重要性:⭐⭐⭐⭐⭐(最大单文件 + UI 中心 + 50+ hooks 编排真实位置src/screens/REPL.tsx5005 行角色:Claude Code 交互模式的根屏幕组件——把所有 hooks、状态、事件、UI 拼成单个 React 组件 关联topics/deep-dive-main.md(main.tsx 的 launchRepl 入口)、topics/deep-dive-query-engine.md(消息循环)


1. 文件全景

REPL.tsx (5005 行)
├── 行 1-220    :imports + 多个 feature-gated 懒加载 hook(50+)
├── 行 221-310  :常量 + utility + stub(PROACTIVE_NO_OP_SUBSCRIBE 等)
│   ├── PROACTIVE_NO_OP_SUBSCRIBE  (行 195)
│   ├── PROACTIVE_FALSE  (行 196)
│   ├── SUGGEST_BG_PR_NOOP  (行 197)
│   ├── RECENT_SCROLL_REPIN_WINDOW_MS  (行 305)
│   └── median()  (行 311)
├── 行 321-470  :TranscriptModeFooter  (行 321)  +  TranscriptSearchBar  (行 368)
├── 行 473-528  :AnimatedTerminalTitle  (行 484)  +  _temp / _temp2 辅助
├── 行 530-570  :Props 类型 (行 526)
├── 行 572-4985 :**REPL 主组件**(~4413 行)— 文件主体
└── 行 4985-5005:AlternateScreen 包装 + 收尾

核心洞察:整个文件的 88% 都是一个 React 函数组件——REPL()


2. 6 个独立函数

function median(values: number[]): number  // 行 311
function TranscriptModeFooter(t0)            // 行 321
function TranscriptSearchBar({...})          // 行 368
function AnimatedTerminalTitle(t0)           // 行 484
function _temp2(setFrame_0)                  // 行 520
function _temp(f)                            // 行 523
export function REPL({...})                  // 行 572  ← 主组件

6 个函数 中,4 个是辅助 / UI 子组件,REPL() 自己是 ~4413 行


3. 60+ 唯一 Hook 列表(grep 验证)

REPL 组件没有拆分成小自定义 hook 之外,调用了 60+ 个 hook

3.1 React 内置(4 个)

  • useState
  • useEffect
  • useLayoutEffect
  • useMemo
  • useCallback
  • useRef
  • useDeferredValue
  • useSyncExternalStore (在 import 里)

3.2 全局状态(3 个)

  • useAppState —— AppStateStore selector(~20+ 次)
  • useAppStateStore —— store 引用
  • useSetAppState

3.3 业务 hooks(50+ 个)

会话/消息: - useLogMessages —— 消息流 append - useReplBridge —— Bridge 协议 - useAssistantHistory —— assistant 历史 - useCanUseTool —— 权限检查 - useQueueProcessor —— 队列处理 - useMessageActions —— 消息操作 - useDeferredHookMessages —— 延迟 hook 消息

IDE/远程: - useIdeLogging / useIdeSelection / useIDEIntegration / useIDEStatusIndicator - useRemoteSession / useDirectConnect / useSSHSession

输入/UI: - useInput —— Ink 全局输入 - useSearchInput / useSearchHighlight —— transcript 搜索 - useTerminalSize - useCommandQueue —— 命令队列 - useKeybindings 相关

通知/状态: - useNotifications —— toast 通知 - useModelMigrationNotifications - useDeprecationWarningNotification - useNpmDeprecationNotification - useAntOrgWarningNotification - useRateLimitWarningNotification - useFastModeNotification - useAutoModeUnavailableNotification - useMcpConnectivityStatus - usePluginInstallationStatus - usePluginAutoupdateNotification - useSettingsErrors - useLspInitializationNotification - useTeammateLifecycleNotification - useChromeExtensionNotification - useOfficialMarketplaceNotification - useInstallMessages - useIssueFlagBanner - useCanSwitchToExistingSubscription

Skills/Plugins: - useSkillsChange —— 监听 skill 文件变更 - useManagePlugins - useMergedClients / useMergedTools / useMergedCommands

proactive / agent: - useProactive / useScheduledTasks - useInboxPoller - useMailboxBridge

安全: - useKickOffCheckAndDisableBypassPermissionsIfNeeded - useKickOffCheckAndDisableAutoModeIfNeeded

模型/认证: - useMainLoopModel —— 当前主循环模型 - useApiKeyVerification - useFileHistorySnapshotInit - useSkillsChange

Voice / 输入扩展: - useVoiceIntegration (feature('VOICE_MODE')) - VoiceKeybindingHandler

Telemetry / Survey: - useFpsMetrics —— FPS 监控 - useFeedbackSurvey - useMemorySurvey - useSkillImprovementSurvey - usePostCompactSurvey - useHint - useCostSummary - useAwaySummary - useFrustrationDetection - useClaudeCodeHintRecommendation - useLspPluginRecommendation - useMoreRight

其他: - useAfterFirstRender - useBackgroundTaskNavigation - useSwarmInitialization - useTeammateViewAutoExit - useLogMessages (上面)

总 60+ 个 hook 调用——REPL编排中心而非业务实现。


4. 顶部:5 类懒加载(行 95-272)

// Voice mode (lazy)
const useVoiceIntegration: typeof import('../hooks/useVoiceIntegration.js').useVoiceIntegration = 
  feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration : () => ({...});
const VoiceKeybindingHandler = feature('VOICE_MODE') ? require(...) : () => null;

// Proactive (lazy)
const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/index.js') : null;
const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
const PROACTIVE_FALSE = () => false;
const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
const useProactive = ... ? require('../proactive/useProactive.js').useProactive : null;
const useScheduledTasks = feature('AGENT_TRIGGERS') ? require(...) : null;

// ANT-ONLY
const AntModelSwitchCallout = "external" === 'ant' ? require(...) : null;
const shouldShowAntModelSwitch = "external" === 'ant' ? require(...) : (): boolean => false;
const UndercoverAutoCallout = "external" === 'ant' ? require(...) : null;

// Web browser tool (lazy)
const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/WebBrowserTool/WebBrowserPanel.js') as typeof import(...) : null;

5 类懒加载

类别 触发条件 兜底
VOICE_MODE feature('VOICE_MODE') 返回 noop
PROACTIVE / KAIROS feature 二者之一 null + noop stub
AGENT_TRIGGERS feature('AGENT_TRIGGERS') null
ANT-ONLY "external" === 'ant' null / false
WEB_BROWSER_TOOL feature null

3 种兜底模式: - null —— module?.foo 调用 - () => ({...}) —— 返回 noop stub 对象 - () => null —— 返回 null 组件

这与 main.tsx 的 lazy require 模式一致——DCE + 运行时安全。


5. Props 接口(行 526-570)

export type Props = {
  // 初始数据(来自 main.tsx / ResumeConversation)
  commands: initialCommands,
  debug,
  initialTools,
  initialMessages,
  pendingHookMessages,
  initialFileHistorySnapshots,
  initialContentReplacements,
  initialAgentName,
  initialAgentColor,
  mcpClients: initialMcpClients,
  dynamicMcpConfig: initialDynamicMcpConfig,

  // 模式开关
  autoConnectIdeFlag,
  strictMcpConfig = false,
  systemPrompt: customSystemPrompt,
  appendSystemPrompt,

  // 回调
  onBeforeQuery,
  onTurnComplete,

  // 状态
  disabled = false,
  mainThreadAgentDefinition: initialMainThreadAgentDefinition,
  disableSlashCommands = false,

  // 远程 / IDE 配置
  taskListId,
  remoteSessionConfig,
  directConnectConfig,
  sshSession,
  thinkingConfig
};

~25 个 props,几乎所有数据都从外部传入(main.tsx 准备好),REPL 主要做编排而非初始化


6. REPL() 主组件 6 段结构

REPL({...})  (行 572-4985, ~4413 行)
├── A. Hooks 初始化段(行 599-1140, ~540 行)— 50+ hooks 调用
│   ├── env-var 缓存(useMemo)
│   ├── useAppState selectors (~20 次)
│   ├── useEffect lifecycle
│   ├── useState (commands, screen, dumpMode, editor, IDE...)
│   ├── useNotifications
│   ├── useMergedClients/Tools/Commands
│   ├── IDE 状态
│   ├── 60+ 个 useXxx() 业务 hook
│   └── keybindings 设置
├── B. Refs + 派生数据段(行 1140-1700, ~560 行)— useRef + useMemo 派生
├── C. 事件处理器段(行 1700-2700, ~1000 行)— onSubmit, onExit, handleXxx
├── D. Query Engine 集成段(行 2700-3500, ~800 行)— useLogMessages, message 流
├── E. Render 前置段(行 3500-4900, ~1400 行)— 大量条件检查 + 状态合并
└── F. Render 段(行 4900-4985, ~85 行)— JSX 输出

7. A 段详解:50+ Hooks 编排

7.1 Env-var 缓存(行 603-608)

const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);
const moreRightEnabled = useMemo(() => "external" === 'ant' && isEnvTruthy(process.env.CLAUDE_MORERIGHT), []);
const disableVirtualScroll = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL), []);
const disableMessageActions = feature('MESSAGE_ACTIONS') ?
  useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MESSAGE_ACTIONS'), []) : false;

注释解释

Env-var gates hoisted to mount-time — isEnvTruthy does toLowerCase+trim+includes, and these were on the render path (hot during PageUp spam).

为什么用 useMemo([]) 而不是顶层 const: - isEnvTruthy 做 toLowerCase+trim+includes(每次 ~微秒级) - 在 PageUp 滚屏时频繁重渲染(每帧) - 提到 mount-time 一次性计算

7.2 AppState selectors(行 617-640)

const [mainThreadAgentDefinition, setMainThreadAgentDefinition] = useState(initialMainThreadAgentDefinition);
const toolPermissionContext = useAppState(s => s.toolPermissionContext);
const verbose = useAppState(s => s.verbose);
const mcp = useAppState(s => s.mcp);
const plugins = useAppState(s => s.plugins);
const agentDefinitions = useAppState(s => s.agentDefinitions);
const fileHistory = useAppState(s => s.fileHistory);
const initialMessage = useAppState(s => s.initialMessage);
const queuedCommands = useCommandQueue();
// ... 20+ 个 selector

特点:每个字段单独 selector(不用 useAppState() 全量)——精确订阅,避免无关字段更新触发重渲染。

7.3 useEffect 局部副作用(行 611-614, 648-671)

// 1. 生命周期 logging
useEffect(() => {
  logForDebugging(`[REPL:mount] REPL mounted, disabled=${disabled}`);
  return () => logForDebugging(`[REPL:unmount] REPL unmounting`);
}, [disabled]);

// 2. Bootstrap local agent
useEffect(() => {
  if (!viewingAgentTaskId || !needsBootstrap) return;
  const taskId = viewingAgentTaskId;
  void getAgentTranscript(asAgentId(taskId)).then(result => {
    setAppState(prev => { ... });
  });
}, [viewingAgentTaskId, needsBootstrap, setAppState]);

bootstrap local_agent 注释

Bootstrap: retained local_agent that hasn't loaded disk yet → read sidechain JSONL and UUID-merge with whatever stream has appended so far. Stream appends immediately on retain (no defer); bootstrap fills the prefix. Disk-write-before-yield means live is always a suffix of disk.

关键: - diskLoaded 标记 - live 是 disk 的后缀(disk-write-before-yield 不变量) - 启动时 fill prefix(diskOnly = disk - live

7.4 60+ useXxx 业务 hooks(行 740-1140)

这一段几乎全是 hook 调用——每个 hook 负责一个独立的"职责":

useModelMigrationNotifications();
useCanSwitchToExistingSubscription();
useIDEStatusIndicator({...});
useMcpConnectivityStatus({...});
useAutoModeUnavailableNotification();
usePluginInstallationStatus();
usePluginAutoupdateNotification();
useSettingsErrors();
useRateLimitWarningNotification(mainLoopModel);
useFastModeNotification();
useDeprecationWarningNotification(mainLoopModel);
useNpmDeprecationNotification();
useAntOrgWarningNotification();
useInstallMessages();
useChromeExtensionNotification();
useOfficialMarketplaceNotification();
useLspInitializationNotification();
useTeammateLifecycleNotification();
useHint();
useCostSummary();
useFeedbackSurvey();
useSkillImprovementSurvey();
useFpsMetrics();
useMemorySurvey();
usePostCompactSurvey();
useFrustrationDetection();
useClaudeCodeHintRecommendation();
useLspPluginRecommendation();
useMoreRight();
// ... 还有

模式: - 每个 hook 自包含全部逻辑(subscription、副作用、cleanup) - REPL 只需要调用它 - 关注点分离:hook 文件 ~50-300 行,REPL 只编排

这与"hooks-first 架构"一致——业务逻辑封装在 hooks,UI 组件保持精简。


8. C 段详解:事件处理器巨兽

事件处理器没有拆成子组件——所有 onSubmit、onExit、handleXxx 都在 REPL 内联。

8.1 onSubmit(推测行 1700-2200)

const onSubmit = useCallback(async (text: string, submitMode: InputMode) => {
  // 1. trim / validate
  // 2. handle slash commands
  // 3. handle bash mode
  // 4. handle agent invocation
  // 5. call QueryEngine
  // 6. handle result / error
  // 7. update history
}, [/* 10+ deps */]);

可能 500 行——单 onSubmit 处理 7+ 种输入模式(slash command、bash、agent、raw prompt、paste、file 等)。

8.2 handleExit(推测行 2000)

优雅退出——保存状态、清理 subscriptions、记录 telemetry。

8.3 50+ onXxx

  • onAutoUpdaterResult
  • onShowMessageSelector
  • onMessageActionsEnter
  • onAgentSubmit
  • onBackgroundSession
  • onRestoreCode
  • onSummarize
  • onRestoreMessage
  • onPreRestore
  • ...

50+ 个事件处理器——都在 REPL 内。


9. D 段详解:Query Engine 集成

const {messages, setMessages, ...} = useLogMessages({...});
const queryModel = ...;

useLogMessages 是 REPL 与 QueryEngine 的桥梁: - 提供 messages(已渲染列表) - setMessages (追加) - 内部调用 runQuery / queryModel

QueryEngine 的输出(流式 SSE 事件)→ useLogMessages 转换为 React state → REPL render。


10. E 段详解:Render 前置(1400 行)

这是最复杂的一段——1400 行全是条件检查 + 状态合并

// 1. IDE 状态派生
const effectiveIdeSelection = ...;
const showIdeOnboarding = ...;

// 2. Tool list 派生
const tools = useMemo(() => mergedTools + dynamicTools, [...]);
const localTools = useMemo(() => getTools(...), [...]);

// 3. Message selector
const messageSelectorPreselect = ...;

// 4. 是否显示 transcript
const isShowingTranscript = screen === 'transcript';

// 5. 其他派生
const isLoading = ...;
const hasSuppressedDialogs = ...;
const focusedInputDialog = ...;
const cursor = ...;

E 段不返回 JSX——只准备所有 render 需要的"原料"。


11. F 段详解:Render(85 行)

const mainReturn = (
  <KeybindingSetup>
    <MCPConnectionManager>
      <Box ...>
        {isShowingTranscript ? (
          <TranscriptView ... />
        ) : (
          <>
            {toolPermissionDialog && <ToolPermissionDialog ... />}
            {/* 几十个 conditional UI */}
            <PromptInput ... />
            <SessionBackgroundHint ... />
          </>
        )}
        {cursor && <MessageActionsBar ... />}
        {focusedInputDialog === 'message-selector' && <MessageSelector ... />}
        {feature('BUDDY') && companionVisible && <CompanionSprite />}
      </Box>
    </MCPConnectionManager>
  </KeybindingSetup>
);

if (isFullscreenEnvEnabled()) {
  return <AlternateScreen mouseTracking={isMouseTrackingEnabled()}>
    {mainReturn}
  </AlternateScreen>;
}
return mainReturn;

3 层包装: - <KeybindingSetup> —— 全局快捷键 - <MCPConnectionManager> —— MCP 连接状态 - <Box> —— Ink 布局

两个 render 路径: - isFullscreenEnvEnabled()(真 TTY)→ <AlternateScreen> 包装(用备用屏幕 buffer,切换时不留痕迹) - 否则直接返回

~85 行 JSX——但 props 极其庞大(每个子组件都有 20+ props)。


12. 关键设计模式

12.1 Hooks-first 架构

业务逻辑 100% 在 hooks 里,REPL 只做编排 + render

对比传统 React: - 传统:组件内 useState + useEffect + 业务函数 - 这里:组件内调用 hooks,没有复杂 effect

12.2 局部 useState vs 全局 AppState

全局(AppStateStore): - 跨组件共享:toolPermissionContext、verbose、mcp、plugins、agentDefinitions - 状态机式:tasks、teamContext、elicitation

局部(useState): - 仅本组件用:screen、dumpMode、editorStatus、IDE 选择 - 临时:inputValue、vimMode

判断标准: - 多处需要 → AppState - 单组件用 → useState

12.3 Lazy hook 加载(行 95-272)

const useVoiceIntegration = feature('VOICE_MODE') ? require('../hooks/useVoiceIntegration.js').useVoiceIntegration : () => ({...});

模式: - feature('X') 编译时门控 - null / () => ({...}) 兜底 - 使用时 module?.foo 判空

效果:商业版不打包 voice 模块(~几 MB 减小)。

12.4 大量 notification hooks

useModelMigrationNotifications();
useDeprecationWarningNotification(mainLoopModel);
useNpmDeprecationNotification();
useAntOrgWarningNotification();
useInstallMessages();
usePluginInstallationStatus();
// ... 20+ 个

为什么这么多:每个通知都是独立 hook——单独订阅、单独条件显示、单独 dismiss。

12.5 useDeferredValue + useDeferredHookMessages

  • useDeferredValue(inputValue) —— 输入不阻塞渲染
  • useDeferredHookMessages —— hook 输出延迟显示

性能优化:用户打字时不卡。

12.6 6 个顶层 helper 维持纯函数

  • median() —— 纯
  • TranscriptModeFooter / TranscriptSearchBar / AnimatedTerminalTitle —— 展示组件
  • _temp / _temp2 —— 微优化辅助

REPL 自身不是纯组件——它有大量副作用。


13. 性能优化技巧

13.1 Env-var 提到 mount-time(行 603-608)

const titleDisabled = useMemo(() => isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_TERMINAL_TITLE), []);

PageUp 滚屏时频繁重渲染——env-var 检查只跑一次

13.2 AppState 精确 selector(行 617-640)

每个字段单独 useAppState(s => s.field)——只在该字段变更时重渲染。

对比useAppState() 全量订阅 → 任何字段变都重渲染。

13.3 useDeferredValue(grep)

const deferredInput = useDeferredValue(inputValue);

打字时:input 立即更新(受控),但昂贵的渲染用 deferredValue(低优先级)。

13.4 useMemo 包裹大计算

const localTools = useMemo(() => getTools(toolPermissionContext), [toolPermissionContext, proactiveActive, isBriefOnly]);

getTools 可能很重——只 3 个 dep 变化时重算。

13.5 useCallback 包裹事件处理器

50+ useCallback —— 防止子组件因 prop 引用变化无谓重渲染。


14. 与其他文件的关系

REPL.tsx
  ├──→ 60+ hooks/ 目录(业务逻辑封装)
  │     ├── useLogMessages(QueryEngine 集成)
  │     ├── useReplBridge(Bridge 协议)
  │     ├── useIdeSelection / useIDEIntegration(IDE)
  │     ├── useProactive(proactive 模式)
  │     └── ...
  ├──→ state/AppStateStore(全局状态)
  ├──→ components/(UI 子组件)
  │     ├── PromptInput
  │     ├── MessageSelector
  │     ├── MessageActionsBar
  │     ├── CompanionSprite
  │     └── ...
  ├──→ ink/(Ink 框架)
  │     ├── Box / Text
  │     ├── useInput
  │     └── AlternateScreen
  ├──→ proactive/(proactive 模块,feature-gated)
  ├──→ voice/(voice 模式,feature-gated)
  ├──→ tools/WebBrowserTool/(feature-gated)
  └──→ 20+ notification hooks

REPL 是"组装工厂"——它 import 一切,编排一切,渲染一切。


15. 复杂度分析

维度 数字
总行数 5005
函数数 6
唯一 hooks 60+
useState 局部 15+
useAppState selector 20+
useEffect / useLayoutEffect 30+
useCallback / useMemo 60+
JSX 元素 ~300
Props 数 25

这是项目里最大的单文件——5005 行 React。


16. 设计哲学

16.1 "God Component" 的合理化

通常 God Component 是反模式。但 Claude Code 的选择是合理的:

  1. 避免 context 套娃——不用 Provider 链
  2. 避免 props drilling——50+ 子组件的 props 都从 REPL 来
  3. 生命周期集中——mount/unmount 在一处
  4. 调试方便——所有逻辑在一个文件

代价:5005 行的"瑞士军刀"。

16.2 Hooks-first 拆解复杂度

REPL 不实现业务——业务在 hooks。REPL 只编排

  • 50+ hook 调用
  • 50+ useCallback
  • 20+ useMemo

结果:单文件 5005 行,但单一职责:"把所有东西装到一起"。

16.3 严格分层

hooks/        (业务逻辑)
REPL.tsx      (编排)
components/   (纯展示)

REPL 不应该 import 业务模块——只 import hooks 和 components。


17. 阅读建议

  1. 不要通读——会被 60+ hooks 淹没
  2. 从 Props 开始(行 526)——看 REPL 接收什么
  3. 看 A 段 50+ hooks 列表(行 599-1140)—— 知道它编排什么
  4. 跳到 F 段 render(行 4900-4985)—— 看输出
  5. 回头看 E 段前置状态(行 3500-4900)—— 理解 render 怎么组装
  6. 不要进 C/D 段(事件 + QueryEngine 集成)—— 那是 hooks 的细节

18. 关键洞察

18.1 单文件 5005 行的"可读性挑战"

  • 主组件 4413 行
  • 60+ hooks
  • 50+ useCallback
  • 20+ useState

项目用大量 section comment(// =====// ---)分段——但单文件仍然巨大

18.2 Hooks-first 架构的优势

  • 业务可测:hooks 可独立测试
  • 关注点分离:UI 编排 vs 业务实现
  • 复用性强:hook 可被其他组件用

18.3 Env-var 性能优化的细节

isEnvTruthy 提到 mount-time(PageUp 滚屏优化)——这是真实性能瓶颈修复

18.4 大量 notification hooks = UX 细节堆砌

20+ 个通知 hook —— 每个都对应一个 UX 细节(模型迁移、deprecation、npm 警告、ant org、插件更新、设置错误、速率限制、auto mode、MCP 连接、LSP 初始化、teammate 生命周期……)。

这是商业产品打磨的痕迹

18.5 ANT-ONLY 和 feature-gated 散布全文

"external" === 'ant'feature('X') 散布——编译时裁剪,不污染运行时。

18.6 useDeferredValue 是"打字不卡"的关键

用户每按一个键,REPL 重渲染——useDeferredValue 让渲染输入解耦。


19. 与其他深度拆解的关系

文件 关系
main.tsx launchRepl 渲染 REPL
replLauncher.ts 提供 initial state + render
state/AppStateStore.ts 全局状态,REPL 通过 useAppState 订阅
hooks/useLogMessages.js 消息流桥接
hooks/useReplBridge.js Bridge 协议
hooks/useXxx.js (50+) 各业务领域封装
components/PromptInput.tsx REPL 的核心子组件(输入框)

20. 阅读清单

  1. ✅ 看 Props 类型(行 526-570)—— 知道 REPL 接什么
  2. ✅ 数 60+ hooks(grep use[A-Z]\w+(
  3. ✅ 看 A 段 hooks 列表(行 599-1140)
  4. ✅ 看 F 段 render(行 4900-4985)
  5. 📌 跳看 1-2 个关键 hook 实现(useLogMessages / useReplBridge
  6. 📌 对照 topics/deep-dive-main.md 理解 launchRepl 怎么到这
  7. 📌 对照 topics/deep-dive-app-state-store.md 理解 AppStateStore

21. 练习任务

  1. 数 60+ hooks(grep + sort -u)—— 看清 REPL 编排的全貌
  2. 找 feature('X') 和 "external" === 'ant' —— 算 REPL 砍了多少代码
  3. 画 REPL 内部模块依赖图 —— 50+ hooks 分几类
  4. 找一个 useEffect —— 解读它清理函数(return () => ...
  5. 思考:如果把 REPL 拆成 10 个小组件,会怎样?会不会更好?
  6. 手写迷你 REPL(~50 行)—— 1 个 useState + 1 个 useEffect + render