4. Push 操作实现:入栈逻辑、页面添加、视图层级管理、回调触发
Push 是导航栈最核心的写入操作,它决定了新页面如何进入栈顶、视图树如何插入、以及上层组件如何感知状态变更。本章从数据模型、视图挂载、回调协议三个层面拆解 push 的实现细节,确保学员能独立实现生产级别的入栈方法。
4.1 入栈数据模型与边界校验
每一次 push 操作都对应一个栈条目(StackEntry)的创建与入栈。数据模型至少需要包含下列字段:
| 字段 | 类型 | 说明 |
|---|---|---|
pageId | String | 页面唯一标识,用于路由匹配与状态记录 |
pageRoute | String | 路由路径,可选,用于深层链接 |
view | View 对象 | 实际渲染的视图实例 |
extra | Map / Bundle | 页面间传递的参数或初始化数据 |
timestamp | Int64 | 入栈时间戳,用于状态恢复排序 |
边界校验要点: push 之前必须检查栈容量上限(
maxStackSize)、页面重复性(是否允许同一 pageId 重复入栈)、以及当前栈是否处于“锁定”状态(动画执行中禁止 push)。
【伪代码:push 前置检查】
function canPush(newEntry):
if stack.isLocked: return false
if stack.count >= stack.maxStackSize: return false
if !stack.allowDuplicate and stack.contains(newEntry.pageId): return false
return true
4.2 页面添加与视图层级管理
当校验通过后,push 方法需要依次完成以下步骤:
- 创建 StackEntry 对象:从路由表或工厂函数获取页面视图实例,并填充元数据。
- 将 entry 推入栈数组尾部:
stack.entries.append(entry)。 - 添加视图到父容器:如果是原生导航,通常使用
addView()或addSubview();如果是 Flutter/React Native,通过虚拟节点挂载。 - 调整视图层级顺序:新页面的 Z-order 必须高于所有旧页面,通常通过
bringToFront()或setChildIndex()实现。 - 触发布局更新:确保新视图完成测量、布局、渲染后才进入下一步。
常见坑点: 视图层级管理容易忽略“前一个页面是否仍然可见”的过渡状态。如果 push 时带入场动画,应在动画期间保持旧页面可见,动画结束后再调整 alpha/可见性。
【Swift/iOS 示例:视图层级插入】
func push(_ entry: StackEntry, animated: Bool) {
// 1. 数据层
navigationStack.append(entry)
// 2. 视图层
containerView.addSubview(entry.view)
containerView.bringSubviewToFront(entry.view)
// 3. 布局约束(略)
entry.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ /* 全屏约束 */ ])
// 4. 动画与回调
if animated {
entry.view.alpha = 0
UIView.animate(withDuration: 0.35) {
entry.view.alpha = 1
} completion: { _ in
self.didPush(entry)
}
} else {
self.didPush(entry)
}
}
4.3 回调触发机制
导航栈的“状态驱动”核心在于一系列回调,push 操作完成后必须依次触发下列回调,用于通知监听者、页面生命周期管理以及持久化记录。
| 回调名称 | 触发时机 | 参数 | 用途 |
|---|---|---|---|
onWillPush(entry) | 校验通过后、视图添加前 | StackEntry | 拦截/修改入栈参数 |
onDidPush(entry) | 视图添加且动画完成 | StackEntry | 通知监听者栈顶变化 |
onStackChanged(oldStack, newStack) | 栈数据变更后 | 原有栈、新栈 | 持久化、状态恢复 |
入栈页面的 onAppear | 视图完全可见 | 无 | 页面生命周期 |
设计决策: 推荐使用“观察者模式”而非硬编码回调列表,以便后期支持多监听者(例如:日志记录 + 状态保存 + 手势拦截器同时订阅)。
【TypeScript 实现:发布-订阅回调】
type StackListener = (event: 'push' | 'pop', entry: StackEntry) => void;
class NavigationStack {
private listeners: StackListener[] = [];
push(entry: StackEntry, animated: boolean) {
if (!this.canPush(entry)) return;
this.emit('willPush', entry);
this.entries.push(entry);
// 视图添加与动画...
this.emit('didPush', entry);
this.emit('stackChanged', this.entries);
}
addListener(fn: StackListener) { this.listeners.push(fn); }
private emit(event, entry) { this.listeners.forEach(l => l(event, entry)); }
}
4.4 Push 整体流程
下图展示了从外部调用 push(Route) 到视图渲染完成的完整流程,包含校验、数据操作、视图管理、回调四个阶段:
4.5 异常场景与防御性编码
即使校验完善,运行时仍可能出现异常,push 实现必须包含兜底逻辑:
- OOM 保护:当视图创建失败时,回滚栈数据并抛出自定义异常。
- 动画中断:如果用户在动画过程中强行关闭应用,应通过
onDispose清理未完成的动画状态。 - 重复点击防护:短时间内连续 push 可能导致多页面重叠;推荐在 push 开始时将按钮置为 disabled,动画结束后恢复。
【Android Kotlin 示例:安全推送】
fun push(route: String, bundle: Bundle? = null) {
if (!canPush()) return
val entry = buildEntry(route, bundle) ?: return // 创建失败则直接返回
stack.add(entry)
supportFragmentManager
.beginTransaction()
.add(R.id.container, entry.fragment, entry.pageId)
.addToBackStack(null)
.commit()
onDidPush(entry)
}
生产环境建议: 栈操作最好封装在单一线程/队列中(主线程),避免并发 push 造成数据竞争。如果使用跨进程导航(如插件化),需要额外加锁。