本节摘要:状态除了组件自己产生,还有两大外来水源——网络与磁盘。本节先回答异步数据的四道必答题(加载、错误、重试、竞态),再讲缓存检索策略如何同时换来「秒开」与「新鲜」,最后为三类端上存储介质画出选型地图,并立下贯穿全册的「随写随存」纪律。学完本节,一个能对抗弱网、能离线展示的数据层就成型了。
场景先摆出来:用户在地铁里打开你的资讯应用,隧道里信号忽有忽无。屏幕是白屏还是上次的内容?加载中有没有骨架屏?请求失败给不给重试按钮?快速切换频道时,慢的旧请求会不会把新频道的结果覆盖掉?——这四问分别对应异步数据的四道必答题:加载态、错误态、重试机制、竞态控制。多数「偶现」的线上数据问题,都能追溯到其中一题没答。
加载与错误是「有没有」的问题,竞态是「对不对」的问题,后者更隐蔽:切到财经频道,财经请求慢;再切回体育,体育先返回渲染;随后财经的迟到响应落地,把体育的内容顶掉——用户看到的是张冠李戴的页面。标准解法有两个方向:让迟到者失效(每次请求带序号或中止信号,渲染前校验「你还是当前生效请求吗」),或让重复请求不再发出(同一参数的在途请求合并)。下面是带中止信号的完整范式:
import React from 'react'; import { Text, ActivityIndicator } from 'react-native'; function useChannelFeed(channelId) { const [state, setState] = React.useState({ status: 'loading', list: [] }); React.useEffect(() => { // 每次频道变化:新建中止控制器,切走时中止上一个在途请求 const ctrl = new AbortController(); setState({ status: 'loading', list: [] }); fetch('https://api.example.com/feed?channel=' + channelId, { signal: ctrl.signal, }) .then(r => r.json()) .then(data => setState({ status: 'success', list: data.list })) .catch(err => { // 中止引发的异常不是真失败,静默即可 if (err.name === 'AbortError') return; setState({ status: 'error', list: [] }); }); return () => ctrl.abort(); }, [channelId]); return state; } function Feed({ channelId }) { const { status, list } = useChannelFeed(channelId); if (status === 'loading') return <ActivityIndicator />; if (status === 'error') return <Text>加载失败,请重试</Text>; return <Text>共 {list.length} 条内容</Text>; }
重试机制的设计要点是「指数退避加次数上限」:失败后等一段再试,间隔逐次拉长,上限之外交给用户手动——无脑自动重试在弱网下只会雪上加霜。这些手工题写上几遍后,你会发现它们高度模式化,这正是请求库(如 React Query 一类方案)兴起的理由:把状态机、去重、缓存、后台刷新做成基建,业务代码只声明「这个键的数据怎么取」。
请求库带来的最大观念升级不是省代码,而是缓存检索策略:拿到数据先展示缓存(哪怕已过期),同时发请求刷新,新数据到了再无缝替换。用户看到的永远是「立即有内容、随后变新鲜」,而不是「转圈等最新」。这套策略成立的前提是缓存有家可归——下图把数据在端上的完整旅程画了出来。

数据要落盘,先选介质。键值异步存储(AsyncStorage 一类)是 RN 生态的老朋友:接口简单、双端一致,适合设置项、标记位、小型缓存;注意 Android 侧默认有总容量限制,存大体积数据要改配置或换介质。高性能键值(MMKV 一类)走同步接口与原生映射内存,读写快一个量级,适合高频小数据的读写(播放进度、界面状态恢复),同步接口还顺手解决了「读缓存也要异步等待」的别扭。关系型轻量库(SQLite 一类)则接住结构化与查询诉求:离线检索、复杂筛选、上万条记录的分页。一句话选型:设置与标记进键值,高频小数据上高性能键值,要查询的结构化数据进关系库。
两条纪律凌驾于选型之上。其一,「随写随存」——回收 2.3 节的伏笔:进程被杀没有告别仪式,重要状态必须在变化的当下就写盘,不能等退出钩子;其二,「版本化迁移」——存储结构会随版本演进,写入时带上版本号,升级时按版本逐级迁移,否则老用户升级后的第一屏就是崩溃现场。这两条纪律会在第 8 章热更新与数据迁移场景里再次接受检验。
补充两个落地时才暴露的边界问题。并发写:键值存储的写操作天然异步,连续快速写入同一键时,完成顺序可能与调用顺序不一致——「先写 A 再写 B」最终落盘 B 覆盖 A 属于正常,而「多来源同时写一个键」(列表页与详情页都更新同一个草稿位)就需要在应用层串行化或分键。容量感知:Android 侧默认总容量有限,接近上限的写入会失败,而写入失败如果被静默吞掉,「随写随存」的承诺就落空了——写失败的告警必须接进监控,这与 8.4 的守护思路一致。iOS 侧容量宽松但有配额检查机制,大体积缓存放标准缓存目录(系统可清理)而不是文档目录(随备份上传),这个目录选择本身也是一种「数据分级」。
背景:产品要求资讯频道页在无网环境下展示上次内容并标注「离线内容」。操作:接入请求库管理频道数据,缓存策略为「先缓存后刷新」;首次成功响应落盘到高性能键值存储,键为频道标识、值为带抓取时间戳的数据快照;启动或进入频道时先读磁盘渲染,再后台刷新;无网且缓存存在时展示缓存并在页头标注时间,无网且无缓存时展示离线引导页;重试按钮走指数退避。结果:地铁场景实测,有缓存频道秒开且内容可读,刷新到货后自动更新;双端行为一致,仅磁盘空间告警文案按平台惯例分别处理。解读:离线能力的本质是「把 4.3 的三层居所连成闭环」,没有一行黑魔法。变式:若列表数据需要支持离线搜索,把快照同步写入关系库并建索引即可——介质可换,层次不变。
数据的血液已接通。下一章让页面之间学会走动:导航与路由,以及双端导航习惯的差异管理。