业务:归集钱包优化
下面按「面试可讲」的结构,梳理 plan-detail.tsx 里你能拿得出手的优化与工程化设计(含相关 hook/store),并标出可改进点,方便你诚实回答追问。
一句话定位
这是一个 计划详情页:大量交易列表 + 实时签名/广播状态 + SQLite 持久化。优化集中在 列表性能、派生计算缓存、跨页面状态、轮询与数据合并,而不是单纯堆 useMemo。
1. 列表与滚动性能
| 措施 | 代码位置 | 面试怎么说 |
|---|---|---|
| FlatList 虚拟化 | FlatList + keyExtractor | 长列表只渲染可视区,比 ScrollView 适合 N 笔交易 |
extraData 精准刷新 | extraData={runtimeTaskUpdateSnapshot} | 轮询/签名更新时让列表知道「哪块数据变了」,避免整页 blind update |
scrollEventThrottle={16} | ~656 行 | 滚动事件约 60fps,配合 Animated 做顶栏渐变 |
筛选后作为 data | data={filteredTransactions} | 筛选在内存完成,列表只绑子集,减少无效 cell |
| 切换筛选滚回顶部 | flatListRef.scrollToOffset | 避免 filter 后停在错误 offset |
<FlatList
ref={flatListRef}
style={styles.scrollView}
data={filteredTransactions}
extraData={runtimeTaskUpdateSnapshot}
renderItem={({ item, index }) => (
<TransactionCard item={item} index={index} chain={chain!} />
)}
keyExtractor={(item) => item.id}
...
onScroll={handleScroll}
scrollEventThrottle={16}
可主动说的缺口:renderItem 是内联函数,TransactionCard 未 React.memo——若被问「还能怎么优化」,答:抽 renderItem + useCallback + 子组件 memo + getItemLayout(固定高度时)。
2. React 渲染与计算缓存
useMemo 做派生数据(避免每次 render 重算)
- 路由参数规范化:
fromPage/planId/sqliteUri(expo-router 可能是数组) coldWalletMap:O(n)建Record,查找冷钱包O(1)filteredTransactions:按activeFilter过滤transactionsToPoll:只对broadcasting且有txHash的项轮询- 业务布尔/统计:
hasSignedTransactions、allTransactionsConfirmed、failedCount/unexecutedCount等
const coldWalletMap = useMemo(() => {
return coldWallets.reduce<Record<string, ColdWallet>>(
(acc, coldWallet) => {
const address = coldWallet.address?.toLowerCase();
if (address) {
acc[address] = coldWallet;
}
return acc;
},
{} as Record<string, ColdWallet>
);
}, [coldWallets]);
useCallback 稳定引用
事件与更新函数:handleMissingAddresses、updatePlanTasks、handleScroll、handleRunCollectionPress 等,减少子组件/hook 因引用变化导致的无效 effect。
useRef 避免无意义 re-render
scrollY、flatListRef、runFlowSessionRef、lastProgressRef- 进度写在 ref 里(传给
usePlanDetailSqlite),恢复签名状态时不必为每次 progress tick 触发整树更新(配合 runtime store)
3. 状态更新:useImmer + 批量 patch
const [planTasks, setPlanTasks] = useImmer<CollectionTransaction[]>([]);
...
const updatePlanTasks = useCallback(
(updates: { transactionId: string; updates: Partial<CollectionTransaction> }[]) => {
setPlanTasks((draft) => {
updates.forEach(({ transactionId, updates: taskUpdates }) => {
const task = draft.find((t) => t.id === transactionId);
if (task) {
...
Object.assign(task, nextUpdates);
}
});
});
},
[setPlanTasks]
);
面试话术:
- Immer:可变写法、不可变语义,批量更新多笔交易时比
{...spread}整数组替换更省分配、更清晰。 updatePlanTasks批量:轮询/ runtime 一次推多条,一次setPlanTasks。- 状态机保护:已是
confirmed/failed时忽略回退到broadcasting的更新,避免 UI 闪烁(业务正确性 + 少无效渲染)。
4. 数据合并:reload 不丢「更新鲜」的链上状态
mergeBroadcastStatusForPlanReload + broadcastStatusRank:DB reload 与内存状态按优先级合并(confirmed > failed > broadcasting),避免轮询已确认却被旧 DB 覆盖。
function broadcastStatusRank(
status: CollectionTransaction['broadcastStatus'] | undefined
): number {
if (status === 'confirmed') return 3;
if (status === 'failed') return 2;
if (status === 'broadcasting') return 1;
return 0;
}
面试亮点:这不只是性能,是 并发/多数据源一致性(SQLite、内存、链上轮询),适合讲「怎么避免 race」。
5. 全局运行时:Zustand 细粒度订阅 + 跨页恢复
const runtimeState = usePlanRuntimeStore((state) =>
planId ? state.runtimeByPlanId[planId] : undefined
);
const runtimeTaskUpdateSnapshot = usePlanRuntimeStore((state) =>
planId ? state.taskUpdateSnapshotByPlanId[planId] : undefined
);
- 按
planId切片订阅,避免订阅整个 store。 effectiveIsSigningPlan:本地 state + runtime 双源合并,离开页面再回来仍能显示「后台在执行」。emitRuntimeTaskUpdates:轮询回调只写 store,再由useEffect同步进planTasks(解耦轮询与 UI 状态)。
6. 逻辑下沉:Custom Hooks(关注点分离)
| Hook | 作用 |
|---|---|
usePlanDetailSqlite | 加载/重载、reloadPlanInFlightRef 防并发重载、isStale 防竞态 |
useSignPlanSqlite | 签名流程 |
useBroadcastResultPollingSqlite | 5s 轮询、tick 互斥、Promise.allSettled 并行查 receipt |
useNavigationHeader | 头部配置 |
页面组件主要负责 组合与展示;面试可说:容器/展示分离、可测、可复用。
相关 hook 里的优化(可一并提):
reloadPlanInFlightRef:重复reloadPlan直接 returnplanLoadParamsRef+isStale():快速切换 plan 时丢弃过期请求- 轮询
isPollingTickRunningRef:防止 tick 重叠 evmServiceRef:RPC 客户端复用,不每 tick new
7. UI/交互层面的「体感性能」
- 顶栏绝对定位 +
listPaddingTop:onLayout量高度,列表内容不被挡;比写死 padding 适配更好。 Animated插值backgroundAlpha:滚动时顶栏背景渐显,用 native driver 友好路径(scrollY来自 scroll)。- 子组件拆分:
PlanDetailOverview、PlanDetailFilter、TransactionCard、Modal 等,减轻单文件重渲染范围(Filter 内还有statusCounts的useMemo)。
8. 轮询范围收窄(省 RPC / 省电)
const transactionsToPoll = useMemo(() => {
...
const pendingTransactions = planTasks
.filter((tx: CollectionTransaction) => {
const txHash = tx.txHash || '';
return txHash && tx.broadcastStatus === 'broadcasting';
})
只对 pending broadcast 轮询;pollingInterval: 5000 控制频率。面试可补:确认/失败后自动从列表移除,轮询集合变小。
9. 工程化与其它
StyleSheet.create外置:样式对象不随 render 重建。- 主题化组件
ThemedView/Text/Button:颜色不进 StyleSheet,符合项目规范。 runFlowSessionRef:带 id 的 session,结束日志可对齐异步signPlan,防旧 session 误结束(异步边界)。- 签名结束触发
reloadPlan:effectiveIsSigningPlanfalse 时同步 DB(数据最终一致)。
面试 STAR 示例(30 秒版)
计划详情页可能有上百笔交易。我用 FlatList + 筛选后的 data + extraData 控制列表更新;用 useMemo 缓存过滤、统计和轮询集合;用 useImmer 批量 patch 更新单笔交易状态,并加了 广播状态优先级合并,避免 DB reload 覆盖链上已确认结果。签名/轮询进度通过 Zustand 按 planId 订阅 支持离开页面仍显示进度;轮询 hook 里 tick 互斥 + 只 poll broadcasting,减少 RPC。顶栏用 onLayout + Animated 做滚动渐变,避免布局写死。
诚实准备的追问
| 追问 | 建议答法 |
|---|---|
为什么不用 React.memo? | 当前优先业务与状态正确性;列表瓶颈在 data 引用与 extraData,下一步给 TransactionCard 加 memo 和稳定 renderItem |
planTasks 变了 Filter 会重算 4 次 filter 吗? | Filter 子组件里 statusCounts 已 memo;父组件也可把统计合成一次 forEach(你父组件 failedCount 已是单次遍历,Filter 里还可优化) |
| 轮询 5s 会不会漏? | 可谈退后台 AppState、指数退避、WebSocket(若链支持) |
| Immer vs Redux? | 本地列表高频 patch 用 Immer 够用;跨页用 Zustand |
如果你希望,我可以再帮你压一版 「英文 1 分钟 elevator pitch」 或 「面试官追问 + 标准答」 清单,直接背即可。