双十一的快递驿站里,小哥阿明面对堆积如山的包裹犯了难:客人问件,他得把整个大厅翻个底朝天。直到站长给他配了三套分拣装备——一层层往上叠的包裹架、按加急/普通/冷链平行摆放的取件格、藏在侧面抽拉出来的抽屉柜——驿站才恢复秩序。这一天,阿明还学会了用专线电话协调总公司,在高峰期包车直送急件。他恍然大悟:移动应用的导航,本质就是一场关于“页面包裹”的调度艺术。 React Native 的导航方案,就是这么一套“驿站装备体系”。React Navigation 是社区主流的分拣台系统,Stack、Tab、Drawer 三种导航器对应三种分拣逻辑,页面栈管理负责包裹单的进进出出,而原生桥接则是一条从驿站打到总公司的专线电话。本文就用阿明的故事,把导航器选型、页面栈管理、传参方式、原生模块通信、性能优化与常见踩坑一次讲透。

岗位关联

掌握这套“页面调度”思维,你不仅能解决 RN 跨平台开发中最棘手的导航问题,还能迁移到任何多页面产品的架构设计——无论做前端还是 AI 应用,栈式流程管理都是底层通用能力。

目录
  1. 可视化演示
  2. 导航器选型
  3. 页面栈管理
  4. 页面传参
  5. 原生桥接
  6. 原生导航选型
  7. 性能优化
  8. 常见踩坑
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Stack导航器的LIFO页面栈push/pop机制、Tab与Drawer导航器的多页面切换与状态保持、以及RN与原生的JS Bridge通信流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Stack导航器LIFO页面栈 navigate检查目标页面是否已在栈中,已存在则跳转不重复压入 push无论目标是否在栈中都压入新页面,适合需要保留历史的操作 pop弹出栈顶页面返回上一页,goBack可指定返回层数 stack.push(page) / stack.pop() LIFO栈管理模式实现页面导航。

导航器选型

双十一的驿站大厅,阿明把包裹全堆在地上,找个件要翻半天。站长给他支招:先分清业务再买装备——包裹要按先后顺序处理,就买一层层往上叠的包裹架(Stack);包裹要按类型平行分区,就买加急/普通/冷链的取件格(Tab);偶尔要打开管理抽屉,就在侧面加一个抽拉柜(Drawer)。导航器的本质,就是给不同页面关系匹配最合适的分拣结构。

React Navigation 中的 Stack、Tab、Drawer 三种导航器,正好对应这三套装备。Stack 导航器强调“层层叠放”,新页面完全盖住旧页面,适合表单填写、商品详情这类“一锤子买卖”的纵深流程;Tab 导航器强调“平行分区”,底部栏几个频道随时切换,适合首页、购物车、我的这类平级模块;Drawer 导航器强调“侧边抽屉”,从屏幕左缘滑出的菜单,适合设置、账户这类低频入口。

选型不必凭感觉。我们可以给每个导航器打分:从层级深度(页面能否无限下钻)、并行切换(平级页面能否快速互达)、手势易用(返回和切换是否符合直觉)三个维度加权求和:

就像驿站先盘点货物再决定买哪套装备,选择导航器前必须梳理页面之间的关系:页面是层层递进还是平级并列?是偶尔进入还是高频切换?把关系盘清楚,选型就顺理成章了。

Score=∑k=13wk⋅fk\text{Score} = \displaystyle\sum_{k=1}^{3} w_k \cdot f_k
  1. 维度下标 kk 从 1 取到 3,对应层级深度、并行切换、手势易用三个评估维度。
    k=1,2,3k = 1, 2, 3
  2. 权重 w_k每个业务对维度的重视程度,比如电商重并行切换,就把 w_2 调大,总和通常设为 1。
    wk∈[0,1], ∑k=13wk=1w_k \in [0,1],\ \sum_{k=1}^{3}w_k = 1
  3. 能力分 f_k某个导航器在维度 k 上的能力评分(0~100),三个加权分数求和得到总分。
    Score=w1⋅f1+w2⋅f2+w3⋅f3\text{Score} = w_1 \cdot f_1 + w_2 \cdot f_2 + w_3 \cdot f_3

页面栈管理

阿明手里有一摞派送单,后到的单子压在最上面,他就必须先把最上面的单送完——这就是“后进先出”。React Navigation 的 Stack 导航器内部维护的页面栈,正是这摞“派送单”。每次调用 navigation.navigate() 或 push(),就像往单摞上压一张新单;按返回键或调用 goBack(),就像送完最上面的单把它抽掉。

栈顶永远指向当前正在展示的页面。理解栈顶的位置,是管理页面返回行为的关键。假设栈里有 L 个页面,索引从 0 开始,那么当前页面(栈顶)的位置是:

除了 push 和 pop,React Navigation 还提供了 replace()(把栈顶的单子换掉,数量不变)和 reset()(把整摞单子清空,只留下指定页面)。登录成功后尤其要用 reset——否则用户按返回键会“退回”登录页,就像送完货回头又撞见一张过期订单,体验非常糟糕。

T=L−1T = L - 1
  1. 栈长度 L当前页面栈里的页面总数,每 push 一次 L 加 1,每 pop 一次 L 减 1。
    L=栈中页面数量L = \text{栈中页面数量}
  2. 栈顶 T顶部页面的索引,等于总长度减一。因为索引从 0 开始,所以第 3 个页面的索引是 2,正好是 L - 1。
    T=顶部页面的索引=L−1T = \text{顶部页面的索引} = L - 1

页面传参

驿站之间转件时,阿明会在包裹单上贴一张备注:收件人、加急标记、附言。下一个驿站看到备注就知道怎么处理。React Navigation 的页面传参就是这张“备注贴纸”:从 A 页面跳转到 B 页面时,把参数塞进 navigation.navigate('B', params) 的第二个参数,B 页面通过 route.params 读取。

参数本质上是一个 JS 对象。目标页面收到的参数,等于跳转时传入的参数展开后,再加上路由自身的 key 等元信息。用展开语法表达这个“贴纸合并”过程:

还有两种“备注”方式:initialParams 像预先印在包裹单上的固定信息,页面第一次挂载就能读到;而“回调回传”则像小哥送完件带回一张回执——通过 navigation.navigate 回到上一页时带上结果。记住:参数别塞大对象,只传 id 这类轻量信息,详细数据放进全局状态再查,否则备注贴纸太厚,反而拖累驿站效率。

Pdst={ ⋯Psrc, k:v }P_{dst} = \{\, \cdots P_{src},\ k: v \,\}
  1. 原参数展开…P_src 表示把源页面传来的参数对象展开,原有字段全部保留。
    ⋯Psrc=源参数的所有字段\cdots P_{src} = \text{源参数的所有字段}
  2. 新增键值对跳转时逐个写入的键 k 和值 v,会追加到参数对象中,形成最终的目标参数。
    k:v=新增的参数字段k: v = \text{新增的参数字段}
  3. 合并结果 P_dst目标页面从 route.params 读到的就是这份合并后的完整对象。
    Pdst=目标页面的 route.paramsP_{dst} = \text{目标页面的 route.params}

原生桥接

驿站经常遇到分拣台搞不定的事:电子签收设备要连总公司系统、摄像头要识别条形码。这时阿明只能拿起专线电话,把需求说给总公司,等对方处理完再回电话——JS 与原生代码的桥接,就是这条专线。React Native 的 NativeModules 对象,就是专线电话的拨号盘:JS 侧调 NativeModules.CameraModule.scan(),原生模块收到消息执行,再把结果回传。

这条专线是异步的,通话不可能同步阻塞——拨号后你得等对方回话。一次桥接调用的总耗时,由三部分组成:

因为专线有固定开销,实战中要尽量少打电话、一次多办事——把多次小调用合并成一次大调用,避免在循环里逐条桥接。另外,原生的耗时操作完成后,可以通过原生事件(如 DeviceEventEmitter)主动“打回电话”通知 JS,而不是让 JS 干等轮询。

Ttotal=TJS+Tbridge+TnativeT_{total} = T_{JS} + T_{bridge} + T_{native}
  1. JS 侧时间 T_JSJS 线程构造调用参数、序列化数据的时间。
    TJS=JS 侧准备消息的时间T_{JS} = \text{JS 侧准备消息的时间}
  2. 桥接传输 T_bridge消息穿过 JS 与原生之间的通信管道的时间,这是专线本身的“占线成本”。
    Tbridge=消息桥接传输时间T_{bridge} = \text{消息桥接传输时间}
  3. 原生执行 T_native原生模块真正干活的时间,比如扫描一张图片、读取一次传感器。
    Tnative=原生侧执行时间T_{native} = \text{原生侧执行时间}

原生导航选型

双十一爆仓那天,普通分拣台忙不过来,阿明干脆包了一辆专车,把加急件直接送到收件人手里。完全改用原生导航(如 react-native-navigation)就是这辆专车:绕开 React Navigation 的 JS 层调度,直接用 iOS 的 UINavigationController 或 Android 的 Fragment 管理页面。优点是响应更快、动画更跟手、与原生 TabBar 的融合更好;代价是要写原生代码,升级和维护成本显著上升。

到底该用社区方案还是原生专车?可以算一笔收益账。设改造成本为 C,长期维护成本为 M,性能与体验提升带来的收益为 B,那么净收益比是:

多数中小团队不需要专职 iOS/Android 工程师维护导航层,选择 React Navigation 就好;只有当应用对导航动画和流畅度有极致要求、且团队原生能力充足时,才值得上专车。也有团队采用混合方案——大部分页面用 React Navigation,少数高频关键路径用原生页面嵌入,相当于“只有加急件才包车”。

Net=BC+M\text{Net} = \displaystyle\frac{B}{C + M}
  1. 收益 B从帧率提升、崩溃减少、用户留存改善中折算的业务价值。
    B=切换后获得的收益B = \text{切换后获得的收益}
  2. 成本 C 与维护 MC 是重写导航层的初始工作量,M 是每次发版维护原生代码的长期开销。
    C+M=初始改造成本+持续维护成本C + M = \text{初始改造成本} + \text{持续维护成本}
  3. 净收益比比值大于 1 才值得“包车”,否则普通分拣台够用。
    BC+M>1⇒值得选择\frac{B}{C+M} > 1 \Rightarrow \text{值得选择}

性能优化

阿明的驿站每年双十一都要优化流水线:把热销货架移到门口(减少走两步)、大件包裹提前搬到传送带旁(缩短搬运时间)、把分拣台升级成自动扫描(省掉重复劳动)。React Native 导航的优化思路如出一辙。

第一招是原生页面渲染。React Navigation 默认每个页面是 JS 视图树,页面多了内存开销大;引入 react-native-screens 后,视图直接在原生层渲染,相当于把分拣台升级成自动扫描线,页面切换不再经过繁重的 JS 布局。第二招是路由懒加载——不常用的页面组件在进入时才加载,就像把冷门包裹先存在仓库深处而不占大厅。第三招是控制 JS 主线程的长任务,保证每一帧不超时。移动设备屏幕通常 60 帧/秒,每一帧的预算时间:

在导航切换的瞬间要避免执行重计算:比如把复杂数据处理放到 requestIdleCallback 里,或在页面进入动画结束后再加载列表数据,就像高峰时段先让包裹跑起来,再慢慢补写登记单。

Tframe=100060≈16.67 msT_{frame} = \displaystyle\frac{1000}{60} \approx 16.67\ \text{ms}
  1. 每秒帧数60 表示屏幕刷新频率为每秒 60 帧。
    60 帧/秒60\ \text{帧/秒}
  2. 单帧预算1000 毫秒除以 60,得到每帧最多约 16.67 毫秒;JS 主线程一旦被长任务阻塞超过这个时间,页面就会有肉眼可见的卡顿。
    Tframe≈16.67 msT_{frame} \approx 16.67\ \text{ms}

常见踩坑

驿站最怕三件事:备注贴纸贴错位置(传参丢失)、旧订单没归档就继续派单(栈未重置)、专线电话占线没人接(桥接超时)。

先说参数丢失。页面在 Tab 切换或 App 从后台恢复时可能重新挂载,重新创建的页面读不到上一次塞进 params 的临时数据。就像小哥换了一班,新来的小哥当然不知道旧包裹单上的备注——解决方法是把关键参数持久化到全局状态(如 Redux)或重新从服务端拉取。再说栈未重置。用户登录后,如果不用 reset 清掉登录页和启动页,按返回键就会“穿越”回登录界面,像送完包裹又绕回仓库,用户体验瞬间崩塌。桥接超时则是最隐蔽的坑:某些原生模块在 JS 侧看起来“没反应”,其实是因为调用链在排队或原生侧抛了异常;对这类调用要加超时保护和日志上报。当页面栈混乱时,可以使用 React Navigation 提供的 navigation.getState() 或调试工具打印当前栈结构:

排查这类问题,最高效的办法不是猜,而是把“栈快照”打印出来,像看监控录像一样复盘每一步 push 和 pop,找到哪一步让页面失去了预期状态。

Snew=∅,Tnew=−1S_{new} = \varnothing,\quad T_{new} = -1
  1. 栈清空reset 操作后,旧栈被清为空集合,登录页等“过期订单”全部移除。
    Snew=∅S_{new} = \varnothing
  2. 栈顶无页面空栈没有栈顶,索引为 -1;随即 push 首页作为新栈的栈底,返回键就不会退回登录态。
    Tnew=−1→push(首页)T_{new} = -1 \rightarrow \text{push(首页)}

AI 应用场景

AI 辅助生成导航配置

阶段:代码自动生成

解决的问题:传统方式下,搭建一个含 Stack + Tab + Drawer 的导航骨架要手动写路由表、注册组件、配置标题栏,反复敲样板代码且容易漏配;用大模型根据一句话描述直接生成完整导航配置,相当于 AI 帮驿站站长画好分拣台布局图,阿明照着安装就行。

具体案例:输入“生鲜电商 App,底部四个频道:首页、分类、购物车、我的;点商品进入详情页;个人中心侧滑出设置菜单”。LLM 输出包含 NavigationContainer、三个 Tab 屏、Stack 嵌套详情页及 Drawer 的完整 App.js 代码,开发者只需接入真实页面组件。

导航埋点驱动用户行为分析

阶段:数据采集与智能分析

解决的问题:页面跳转链条长,用户到底在哪个节点流失,人工看日志难以发现;借助导航栈的 push/pop 埋点和聚类算法,把“用户路径”当做包裹流转记录来分析,找出频繁放弃的环节,再优化导航层级。

具体案例:App 在每次 push 时上报“来源页 → 目标页”,AI 系统聚类后发现 70% 的用户在“填写地址 → 确认订单”之间退出,原因是订单确认页被埋在第三层且返回键会清空表单。产品据此把确认页提前到二层,转化率提升 12%。

桥接调用智能排障

阶段:运维与可观测性分析

解决的问题:NativeModules 调用链路长、失败原因隐蔽(JS 参数序列化问题、原生线程阻塞、返回值格式错误);传统排查要在两端打日志手动对照,AI 则把桥接日志和错误堆栈喂给大模型做根因分析,相当于给专线电话装了自动录音和故障诊断。

具体案例:一段扫码模块的桥接调用出现“偶发超时”,AI 分析 3 万条调用日志后定位:高并发时原生侧 Bitmap 解码导致线程池打满,并自动生成优化建议——改用异步线程池并合并批量调用,修复后超时率下降 90%。

Python 演示

python43 行
"""模拟 React Navigation 的 Stack 导航器:压栈、出栈、传参"""class StackNavigator:    def __init__(self, routes):        self.routes = routes       # 路由名 -> 页面名        self.page_stack = []       # 页面栈        self.route_params = {}     # 页面名 -> 参数     def push(self, page_name, **params):        if page_name not in self.routes:            raise ValueError("未注册的路由: {}".format(page_name))        self.page_stack.append(page_name)        self.route_params[page_name] = params        print("[push] 进入 {},页面栈: {}".format(page_name, self.page_stack))     def pop(self):        page_name = self.page_stack.pop()        print("[pop] 离开 {},页面栈: {}".format(page_name, self.page_stack))        return page_name     def replace(self, page_name):        self.page_stack[-1] = page_name        print("[replace] 替换为 {},页面栈: {}".format(page_name, self.page_stack))     def top_page(self):        return self.page_stack[-1] if self.page_stack else None     def get_params(self, page_name):        return self.route_params.get(page_name)     def reset_to(self, page_name):        self.page_stack = [page_name]        print("[reset] 重置为 {},页面栈: {}".format(page_name, self.page_stack)) if __name__ == "__main__":    nav = StackNavigator(routes={"Home": "首页", "Detail": "详情", "Cart": "购物车"})    nav.push("Home")    nav.push("Detail", goods_id=101)    nav.push("Cart")    nav.pop()    nav.replace("Detail")    nav.reset_to("Home")    print("当前页面:", nav.top_page())    print("Detail 参数:", nav.get_params("Detail"))
目的:用纯 Python 复刻 Stack 导航器的核心数据结构,直观演示 push、pop、replace、reset 四类页面栈操作。
  1. self.page_stack = []页面栈本体,列表的尾部就是栈顶,对应 React Navigation 的内部栈。
  2. nav.push("Detail", goods_id=101)页面传参的直观写法:路由名 + 参数字典,对应 navigate('Detail', { goodsId: 101 })。
  3. self.page_stack.append(page_name)压栈就是往列表尾部追加元素,后进者先被 pop。
  4. nav.pop()出栈移除栈顶,返回的正是最后进入的"Cart"页面,体现后进先出。
  5. nav.replace("Detail")替换栈顶而不改变长度,常用于“登录页 → 首页”这类不想保留历史页的场景。
  6. nav.reset_to("Home")清空整栈并只保留首页,模拟登录后重置导航状态,防止返回键退回登录页。

常见误区

误区看示例代码时只注意 `Stack.Navigator` 和 `Screen`,以为 NavContainer 是可选包装,直接省略也能跑。

正解NavigationContainer 是整个导航系统的“驿站总台”——它负责持有导航状态、把路由分发给对应页面、监听系统返回键。没有总台,分拣台再多也无法运转,应用启动就会报错。写任何导航代码前,第一行必须是 <NavigationContainer> 包裹。

为何常见:示例代码常把 Container 放在最外层,读者默认它是“框架脚手架”而不是业务逻辑的一部分,删除后报错信息又不够直观,于是反复踩坑。

误区从 A 跳到 B 再想回 A,直接 `navigate('A')` 就行,和 Web 的 `location.href` 一样省事。

正解navigate 会尝试复用已有的 A 页面,不一定生成新页面;但用户的预期是“回到上一个页面”(返回行为),应该用 goBack() 或 pop()。navigate 更适合“跳转到指定频道页”,goBack 才对应系统的物理返回键语义。

为何常见:web 开发者习惯了 URL 跳转的“直达”思维,忽略了移动端返回手势和系统返回键都由页面栈驱动,把“去某个页面”和“回上一页”混为一谈。

误区跳转时把整个用户对象、整个列表塞进 `params`,省得目标页面再查一次数据,感觉最方便。

正解React Navigation 推荐 params 只传 id 等可序列化的轻量信息,传大对象会导致状态不一致(比如对象在 A 页被修改,B 页读到的却是旧引用)、触发序列化告警、拖慢导航动画。正确做法是:大对象放全局状态(Redux/Zustand),目标页拿 id 去取。

为何常见:demo 和小项目传小对象看不出问题,等状态多起来才暴露;“传对象最省事”的惯性让问题被长期掩盖。

误区`NativeModules.CameraModule.scan()` 写完直接拿返回值,就像普通函数调用一样,结果经常是 undefined。

正解JS 与原生之间的桥接是异步消息通道——调用像拨电话,要等对方回话,结果通过 Promise、回调函数或原生事件返回。所有耗时原生操作都应当按异步模式设计,不能指望同步拿结果。

为何常见:桥接 API 的调用语法和同步函数太像,加上部分社区模块封装了 Promise 让调用方无感,开发者容易忽略底层其实是“发消息 + 收消息”两段过程。

误区登录成功直接 `navigate('Home')` 就行,用户能看到首页即一切正常。

正解登录页、启动页、引导页都残留在栈里,用户按返回键会一步步“穿越”回登录界面。正确做法是用 reset 把栈重建为 [Home],像驿站送完货后把旧单全部归档,只保留当前有效的一摞单。

为何常见:不打印栈结构就看不到隐藏页面,开发时一直点首页按钮发现不了问题,直到真机测试按返回键才暴露。

面试题

Q1. React Navigation 的页面栈遵循哪种出栈顺序?

单选题 · easy

  • A.先进先出(FIFO)
  • B.后进先出(LIFO)← 正确答案
  • C.随机优先
  • D.按优先级排序

答案:B。页面栈像阿明手里的派送单,后到的压在上面先送。push 往栈顶加页面,pop 从栈顶移除,所以遵循后进先出(LIFO)。先进先出是队列的特征,随机和优先级排序都与栈结构无关。

Q2. 以下哪个是 React Navigation 的根容器,必须包裹所有导航器?

单选题 · easy

  • A.Stack.Navigator
  • B.Tab.Navigator
  • C.NavigationContainer← 正确答案
  • D.Drawer.Navigator

答案:C。NavigationContainer 相当于驿站总台,负责持有导航状态和分发路由,必须是最外层;三个 Navigator 都是具体的分拣台,必须挂在总台之下才能工作。

Q3. 栈中有 5 个页面,当前栈顶页面的索引是多少?

单选题 · mid

  • A.3
  • B.4← 正确答案
  • C.5
  • D.6

答案:B。索引从 0 开始,栈长度为 L 时栈顶索引是 L - 1,所以 5 个页面的栈顶索引是 4。选 3 是把长度当成索引,选 5 是忘了从 0 开始计数。

Q4. 从页面 A 跳转到页面 B 时,传给 B 的参数放在哪个字段?

单选题 · mid

  • A.route.name
  • B.route.params← 正确答案
  • C.navigation.state
  • D.navigation.key

答案:B。跳转时 navigate('B', params) 的第二个参数会挂到 B 页面的 route.params 上,B 通过 route.params 读取。route.name 是路由名,key 和 state 是导航内部元信息,都不是业务参数的载体。

Q5. 用户登录成功后,为了防止返回键退回登录页,应该执行什么操作?

单选题 · mid

  • A.navigate('Home')
  • B.push('Home')
  • C.replace('Home')
  • D.reset 栈并仅保留 Home← 正确答案

答案:D。navigate 和 push 都会把登录页留在栈里,replace 只替换栈顶但登录页之前可能还有启动页;只有 reset 能把整个栈清空并重建为 [Home],保证返回键不会回到登录态。

Q6. NativeModules 的桥接调用本质是什么?

单选题 · mid

  • A.同步阻塞调用
  • B.异步消息通信← 正确答案
  • C.WebSocket 连接
  • D.HTTP 请求

答案:B。JS 与原生之间的桥接是专线电话式的异步消息:JS 发消息、原生执行完再回消息,结果通过回调或 Promise 返回。它不是同步阻塞,内部也不是走网络协议。

Q7. 在页面栈中执行 replace('Home') 后,栈内页面数量如何变化?

单选题 · hard

  • A.增加 1
  • B.减少 1
  • C.保持不变← 正确答案
  • D.清空为 0

答案:C。replace 把栈顶页面替换成新页面,相当于送完当前单子换一张新单,页数不变。增加是 push 的效果,减少是 pop 的效果,清空是 reset 的效果。

Q8. 以下哪些属于 React Navigation 内置的导航器类型?

多选题 · mid

  • A.Stack 导航器← 正确答案
  • B.Tab 导航器← 正确答案
  • C.Drawer 导航器← 正确答案
  • D.原生 Fragment 导航器

答案:ABC。Stack、Tab、Drawer 是 React Navigation 提供的三类分拣台;原生 Fragment 导航器属于 Android 原生概念,不是 React Navigation 内置类型,需要桥接或使用完全原生的导航库。

Q9. 哪些措施可以有效优化 React Native 导航性能?

多选题 · mid

  • A.引入 react-native-screens← 正确答案
  • B.对不常用页面做懒加载← 正确答案
  • C.在导航动画期间执行重计算
  • D.合并多次桥接调用← 正确答案

答案:ABD。react-native-screens 让页面在原生层渲染,懒加载减少首屏负担,合并桥接调用减少专线占线成本,都是有效优化;在导航动画期间执行重计算会卡顿,属于反面做法。

Q10. 以下哪些情况会导致页面参数(route.params)意外丢失?

多选题 · hard

  • A.Tab 切回后组件重新挂载← 正确答案
  • B.页面从后台恢复时状态重建← 正确答案
  • C.使用 reset 重建页面栈← 正确答案
  • D.调用 getState 查看栈信息

答案:ABC。Tab 切换、后台恢复、reset 重建都会导致页面重新挂载,新实例读不到上一次塞进 params 的临时数据,应把关键数据持久化到全局状态。调用 getState 只读不改,不会导致参数丢失。