@[toc]
前言:这不是“谁更好”,而是“谁解决的问题不同”
很多团队在同时做 Web 和 RN 项目时,都会下意识问一句:
Vue Router 这套东西,在 RN 里能不能也照着来?
如果你只是做 Demo,答案是「看起来可以」。
但只要项目一上规模,你就会发现:照抄一定会出问题,而且问题还很隐蔽。
这篇文章不打算教你 API,而是想把一个问题讲清楚:
RN Navigation 和 Vue Router 的“路由”,根本就不是一类东西。
我会从几个你一定踩过的点展开:
- 页面生命周期为什么完全不一样
- 内存为什么 RN 更容易出问题
- 返回行为到底是谁在控制
- 权限、深链、工程结构该怎么设计才不乱
一、先把一个误区拆掉:它们真的不是同一类“路由”
1. Vue Router 本质是在“切状态”
在 Web 里,路由更像一个状态映射器:
URL 变化 → 匹配路由 → 渲染组件页面是否存在,完全由当前 URL 决定。
你离开这个路由,对应的组件直接销毁,内存自然回收。
这也是为什么在 Vue 里你可以很自然地写:
onMounted(()=>{fetchData()})onUnmounted(()=>{clearInterval(timer)})基本不会出什么大事。
2. RN Navigation 管的是“真实页面栈”
RN 不一样。
RN Navigation 管的是一个真实存在的页面栈,更接近原生:
push → 页面入栈 pop → 页面出栈页面不是因为“当前路径不匹配”消失的,而是:
你有没有把它从栈里移走。
这个差异,决定了后面 80% 的坑。
二、生命周期差异:为什么 RN 项目更容易“慢慢变卡”
1. Vue 的生命周期很“干脆”
Vue 页面的一生通常是:
创建 → mounted → 使用 → unmounted → 回收离开就是离开,不存在“后台挂着”的说法。
2. RN Screen:存在 ≠ 可见
在 RN Navigation 里,一个 Screen 的生命周期更像这样:
mount → focus → blur → focus → blur → (可能永远不 unmount)注意这句话:
默认情况下,页面被 push 进栈后,是不会卸载的。
这会直接导致几个经典问题:
useEffect([])只执行一次,但页面反复进入- 定时器、订阅、listener 一直存在
- 页面看似“关闭了”,其实还在内存里
3. 正确的 RN 生命周期使用方式
在 RN 里,真正靠谱的不是 mount/unmount,而是 focus/blur。
推荐写法是:
import{useFocusEffect}from'@react-navigation/native'useFocusEffect(React.useCallback(()=>{fetchData()return()=>{cancelRequest()}},[]))这段代码解决的是一个核心问题:
页面存在,但是否“正在被用户使用”。
三、内存模型:为什么 RN 更容易 OOM
1. Vue:页面就是临时对象
在 Web 里:
- 页面组件是 JS 对象
- 不可达就被 GC
- DOM 节点也随之销毁
你几乎不用关心“页面堆积”。
2. RN:页面是“重量级资源”
RN 的 Screen 背后是:
- Native View
- 布局树
- 图片缓存
- 手势、动画上下文
如果你一个 Stack 里压了十几个复杂页面,它们默认全都还活着。
3.unmountOnBlur是不是万能解?
很多人看到这个配置就松了一口气:
<Stack.Screen name="Detail"component={DetailScreen}options={{unmountOnBlur:true}}/>但真实项目里,你很快会发现:
- 页面状态丢失
- 回退重新请求接口
- 滚动位置全没了
所以它更适合:
- 表单完成页
- 一次性流程页
- 不需要状态保留的页面
而不是“全局开启”。
四、返回栈:为什么 RN 的复杂度更高,但也更强
1. Vue Router 没有“返回栈”的概念
Web 的返回,本质是:
浏览器历史栈Vue Router 只是配合 URL 在工作。
2. RN Navigation 是显式栈
RN 里你必须明确地管理:
navigation.push('Detail',{id})navigation.pop()navigation.reset(...)这带来的问题是:
- 栈结构不清晰,很容易乱
- 多人协作时容易 push 错页面
3. 工程级建议:限制导航入口
一个成熟 RN 项目,通常会做两件事:
1. 封装 navigate 方法 2. 禁止跨模块随意跳转示例:
exportfunctiongoToUserDetail(id){navigationRef.navigate('UserDetail',{id})}这样可以:
- 统一参数结构
- 避免随意拼路由名
- 后期好重构
五、权限模型:拦截式 vs 组合式
1. Vue Router:集中拦截
router.beforeEach((to,from,next)=>{if(!hasPermission(to))next('/403')elsenext()})简单、直接、集中。
2. RN Navigation:页面根本不存在
RN 更推荐这种方式:
function RootNavigator() { return isLogin ? <AppStack /> : <AuthStack /> }甚至在 AppStack 内部:
{isAdmin && ( <Stack.Screen name="Admin" component={AdminScreen} /> )}逻辑非常直观:
你没权限,这个页面实例压根就不会被创建。
六、实战 Demo:一个列表页 + 详情页的差异对比
RN Navigation Demo
functionListScreen({navigation}){return(<Button title="去详情"onPress={()=>navigation.push('Detail',{id:1})}/>)}functionDetailScreen({route}){const{id}=route.paramsuseFocusEffect(React.useCallback(()=>{console.log('页面可见,加载数据')return()=>{console.log('页面失焦,清理资源')}},[id]))return<Text>详情页{id}</Text>}重点不是代码,而是思路:
- 不依赖 unmount
- 所有副作用都和 focus 绑定
Vue Router 对应 Demo
onMounted(()=>{fetchData(route.params.id)})onUnmounted(()=>{cancelRequest()})这里你不需要关心“页面是否还在后台”。
七、总结:什么时候该用哪种思维?
一句话总结:
- Vue Router 是状态驱动视图
- RN Navigation 是页面驱动体验
如果你在 RN 项目里:
- 把页面当成“用完就没了”
- 用 Web 的生命周期思维写代码
- 不约束导航结构
那么项目越做越慢,几乎是必然的。
反过来,如果你:
- 接受页面长期存在
- 用 focus/blur 管理副作用
- 把导航当成架构的一部分
RN Navigation 反而会非常稳定、可控。