“浏览器滚动归零” 问题

0 阅读3分钟

这是一个非常经典的 “浏览器滚动归零” 问题,特别是在使用 window(全局滚动)结合 keep-alive 切换组件时经常发生。

为什么在 onDeactivated 时获取到的值是 0?

核心原因:DOM 树的连动反应与高度塌陷。

  1. 高度瞬间消失:当你切换 Tab 时,Vue 的 keep-alive 会将当前的组件从 DOM 中移除(放入内存)。此时,.transaction-content-wrapper 里的内容瞬间消失。
  2. 高度塌陷:因为你使用的是浏览器的 window 滚动,页面的总高度取决于内容高度。当旧组件消失、新组件还没挂载/渲染出高度时,整个页面的高度会瞬间缩短为 Header 的高度。
  3. 浏览器自动重置:如果页面总高度变得比窗口高度还小,浏览器会自动把 window.scrollY 重置为 0
  4. 钩子触发顺序onDeactivated 触发时,组件其实已经“脱离”了活跃文档流,在这个瞬间,浏览器可能已经完成了高度计算并把滚动条归零了。所以你拿到的永远是 0

解决方案

既然在“销毁/停用”瞬间拿不到正确的值,最稳妥的方法是:在滚动发生时实时记录,或者在切换发生前提前记录

方案一:实时监听滚动(推荐,最稳妥)

在子组件内实时记录当前的滚动位置,这样无论什么时候停用,我们手里都有最新的位置。

// TransactionList.vue

// 1. 修改记录变量
const currentScrollY = ref(0)

// 2. 滚动处理函数
const handleScroll = () => {
    // 只有在组件激活状态下才记录
    currentScrollY.value = window.scrollY || document.documentElement.scrollTop
}

onMounted(() => {
    window.addEventListener('scroll', handleScroll)
})

onBeforeUnmount(() => {
    window.removeEventListener('scroll', handleScroll)
})

/** 修改保存逻辑:不再从 DOM 获取,而是直接用实时记录的值 */
const saveScrollPosition = () => {
    savedScrollPosition.value = currentScrollY.value
    console.log('💾 从实时变量保存位置:', savedScrollPosition.value)
}

// onDeactivated 依然保留,但它现在只是把实时值存入 savedScrollPosition
onDeactivated(() => {
    saveScrollPosition()
})

方案二:在父组件切换前触发记录

在父组件 activeTabIndex 改变之前,通知子组件保存。但由于你的子组件是在 keep-alive 里的,通过 ref 调用子组件方法最稳。

父组件修改:

// Transaction.vue

// 监听 tab 切换,在切换前手动触发保存(如果需要更精细控制)
watch(activeTabIndex, (newVal, oldVal) => {
    // 这里如果能拿到旧组件的实例,可以手动调用 save
    // 但 Vue 的 keep-alive 动态组件通过 ref 拿到的是当前激活的
    // 所以实时监听方案(方案一)通常是唯一的选择
})

方案三:给容器设置最小高度

为了防止切换时高度塌陷导致滚动条跳动,可以给父组件的容器设置一个最小高度。

// Transaction.vue
.transaction-content-wrapper {
    min-height: 100vh; // 确保即使内容为空,滚动条也不会因为高度塌陷而瞬间归零
    padding: 0 10px;
}

优化后的子组件代码建议

建议对你的恢复逻辑做一点点延迟处理,因为 window.scrollTo 在内容还没完全渲染出来时执行可能会失败:

/** 恢复滚动位置 */
const restoreScrollPosition = () => {
    // 使用 nextTick 有时候不够,因为 API 请求可能是异步的
    // 如果是恢复 keep-alive 缓存的内容,nextTick 通常足够
    nextTick(() => {
        setTimeout(() => {
            window.scrollTo({
                top: savedScrollPosition.value,
                behavior: 'instant' // 使用瞬间跳转,避免平滑滚动导致 onLoad 重复触发
            })
            console.log('📍 恢复滚动位置:', savedScrollPosition.value)
        }, 10) // 给 10ms 的缓冲时间让浏览器完成渲染
    })
}

总结

  1. 不要在 onDeactivated 里通过 window.scrollY 取值,因为那时候页面高度可能已经塌陷。
  2. 改用 window.addEventListener('scroll', ...) 在子组件内实时更新一个变量。
  3. 确保容器有 min-height,防止切换瞬间页面长度归零。