4. Push 操作实现:入栈逻辑、页面添加、视图层级管理、回调触发

Push 是导航栈最核心的写入操作,它决定了新页面如何进入栈顶、视图树如何插入、以及上层组件如何感知状态变更。本章从数据模型、视图挂载、回调协议三个层面拆解 push 的实现细节,确保学员能独立实现生产级别的入栈方法。

4.1 入栈数据模型与边界校验

每一次 push 操作都对应一个栈条目(StackEntry)的创建与入栈。数据模型至少需要包含下列字段:

字段类型说明
pageIdString页面唯一标识,用于路由匹配与状态记录
pageRouteString路由路径,可选,用于深层链接
viewView 对象实际渲染的视图实例
extraMap / Bundle页面间传递的参数或初始化数据
timestampInt64入栈时间戳,用于状态恢复排序
边界校验要点: 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 方法需要依次完成以下步骤:

  1. 创建 StackEntry 对象:从路由表或工厂函数获取页面视图实例,并填充元数据。
  2. 将 entry 推入栈数组尾部:stack.entries.append(entry)。
  3. 添加视图到父容器:如果是原生导航,通常使用 addView() 或 addSubview();如果是 Flutter/React Native,通过虚拟节点挂载。
  4. 调整视图层级顺序:新页面的 Z-order 必须高于所有旧页面,通常通过 bringToFront() 或 setChildIndex() 实现。
  5. 触发布局更新:确保新视图完成测量、布局、渲染后才进入下一步。
常见坑点: 视图层级管理容易忽略“前一个页面是否仍然可见”的过渡状态。如果 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) 到视图渲染完成的完整流程,包含校验、数据操作、视图管理、回调四个阶段:

Push 操作流程图 Push 操作全流程 外部调用 push() 边界校验 创建 StackEntry 入栈 & 视图添加 触发回调序列 栈锁定 / 容量 / 重复 检查不通过则直接返回 视图添加到容器 管理 Z-order willPush → didPush → 生命周期

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 造成数据竞争。如果使用跨进程导航(如插件化),需要额外加锁。