Skip to main content

业务:归集钱包优化

下面按「面试可讲」的结构,梳理 plan-detail.tsx 里你能拿得出手的优化与工程化设计(含相关 hook/store),并标出可改进点,方便你诚实回答追问。


一句话定位

这是一个 计划详情页:大量交易列表 + 实时签名/广播状态 + SQLite 持久化。优化集中在 列表性能、派生计算缓存、跨页面状态、轮询与数据合并,而不是单纯堆 useMemo


1. 列表与滚动性能

措施代码位置面试怎么说
FlatList 虚拟化FlatList + keyExtractor长列表只渲染可视区,比 ScrollView 适合 N 笔交易
extraData 精准刷新extraData={runtimeTaskUpdateSnapshot}轮询/签名更新时让列表知道「哪块数据变了」,避免整页 blind update
scrollEventThrottle={16}~656 行滚动事件约 60fps,配合 Animated 做顶栏渐变
筛选后作为 datadata={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 是内联函数,TransactionCardReact.memo——若被问「还能怎么优化」,答:renderItem + useCallback + 子组件 memo + getItemLayout(固定高度时)


2. React 渲染与计算缓存

useMemo 做派生数据(避免每次 render 重算)

  • 路由参数规范化:fromPage / planId / sqliteUri(expo-router 可能是数组)
  • coldWalletMapO(n)Record,查找冷钱包 O(1)
  • filteredTransactions:按 activeFilter 过滤
  • transactionsToPoll:只对 broadcasting 且有 txHash 的项轮询
  • 业务布尔/统计hasSignedTransactionsallTransactionsConfirmedfailedCount / 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 稳定引用

事件与更新函数:handleMissingAddressesupdatePlanTaskshandleScrollhandleRunCollectionPress 等,减少子组件/hook 因引用变化导致的无效 effect。

useRef 避免无意义 re-render

  • scrollYflatListRefrunFlowSessionReflastProgressRef
  • 进度写在 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签名流程
useBroadcastResultPollingSqlite5s 轮询、tick 互斥、Promise.allSettled 并行查 receipt
useNavigationHeader头部配置

页面组件主要负责 组合与展示;面试可说:容器/展示分离、可测、可复用

相关 hook 里的优化(可一并提):

  • reloadPlanInFlightRef:重复 reloadPlan 直接 return
  • planLoadParamsRef + isStale():快速切换 plan 时丢弃过期请求
  • 轮询 isPollingTickRunningRef:防止 tick 重叠
  • evmServiceRef:RPC 客户端复用,不每 tick new

7. UI/交互层面的「体感性能」

  • 顶栏绝对定位 + listPaddingToponLayout 量高度,列表内容不被挡;比写死 padding 适配更好。
  • Animated 插值 backgroundAlpha:滚动时顶栏背景渐显,用 native driver 友好路径(scrollY 来自 scroll)。
  • 子组件拆分PlanDetailOverviewPlanDetailFilterTransactionCard、Modal 等,减轻单文件重渲染范围(Filter 内还有 statusCountsuseMemo)。

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 误结束(异步边界)。
  • 签名结束触发 reloadPlaneffectiveIsSigningPlan false 时同步 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」「面试官追问 + 标准答」 清单,直接背即可。