bug处理

This commit is contained in:
truewhile
2026-09-18 22:52:09 +08:00
parent 61a7260eaf
commit 0810bfb234
2 changed files with 104 additions and 9 deletions
+40 -1
View File
@@ -1,4 +1,8 @@
import { shouldPersistScrollSample, shouldRememberScroll } from './useScrollMemory.ts'
import {
shouldPersistClampedSample,
shouldPersistScrollSample,
shouldRememberScroll,
} from './useScrollMemory.ts'
function check(name: string, condition: boolean) {
if (!condition) throw new Error(`useScrollMemory: ${name}`)
@@ -48,4 +52,39 @@ check(
}),
)
// 恢复途中内容还没长出来(滚不动)或被钳在矮内容底部时,scrollTop 是浏览器钳制
// 出来的值,不能写回存储,否则一次误触就把记忆位置清成 0。
check(
'clamped sample while list is still loading is ignored',
!shouldPersistClampedSample({ current: 0, maxScroll: 0, saved: 1800 }),
)
check(
'clamped sample at the bottom of a shrunken list is ignored',
!shouldPersistClampedSample({ current: 1200, maxScroll: 1200, saved: 1800 }),
)
check(
'sample at the bottom of a completely scrolled list stays persistable',
shouldPersistClampedSample({ current: 1800, maxScroll: 1800, saved: 1800 }),
)
check(
'reachable scroll position is persisted',
shouldPersistClampedSample({ current: 900, maxScroll: 1200, saved: 1800 }),
)
check(
'reaching the remembered target is persisted',
shouldPersistClampedSample({ current: 1800, maxScroll: 2600, saved: 1800 }),
)
check(
'deeper than the remembered target is persisted',
shouldPersistClampedSample({ current: 3000, maxScroll: 4000, saved: 1800 }),
)
check(
'intentional scroll to top of a tall list is persisted',
shouldPersistClampedSample({ current: 0, maxScroll: 4000, saved: 1800 }),
)
check(
'unscrollable page without memory cannot persist',
!shouldPersistClampedSample({ current: 0, maxScroll: 0, saved: 0 }),
)
console.log('useScrollMemory.test.ts ok')
+64 -8
View File
@@ -56,9 +56,35 @@ export function shouldPersistScrollSample(input: {
return true
}
/**
* 判断一次滚动采样是不是“用户真正选中的位置”。
*
* 内容还在加载(容器根本滚不动)或被钳在“变矮内容”的底部时,scrollTop 是浏览器
* 钳制出来的值:恢复途中任何一次滚轮 / 触摸 / 按键都会读到这个值,写回存储就会把
* 记忆位置抹成 0(表现为返回媒体库时永远停在顶部,且之后再也不会记录)。
*/
export function shouldPersistClampedSample(input: {
current: number
maxScroll: number
saved: number
}): boolean {
const { current, maxScroll, saved } = input
if (maxScroll <= 0) {
return false
}
if (current >= maxScroll && current < saved) {
return false
}
return true
}
/**
* 记住列表页的滚动位置。页面内容会异步长高,因此恢复期间会监听内容高度,
* 直到目标位置可达;期间用户主动滚动会立即接管,避免和恢复逻辑抢滚动条。
*
* 恢复还没完成时,容器里的 scrollTop 往往是浏览器钳制出来的值(内容还在加载
* 占位,或被钳在变矮内容的底部)。这种采样既不写回存储,也不当成用户“接管”,
* 否则一次误触的滚轮 / 触摸就会把记忆位置清成 0。
*/
export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
useLayoutEffect(() => {
@@ -80,6 +106,19 @@ export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
let lastSaved = saved
let lastHeight = el.scrollHeight
const currentMaxScroll = () => Math.max(0, el.scrollHeight - el.clientHeight)
// 只有“用户真正能滚到”的位置才算有效采样:内容还在加载占位(滚不动)或
// 被钳在变矮内容的底部时,scrollTop 都是浏览器钳制出来的值。
const sampleIsTrustworthy = (current: number) =>
shouldPersistClampedSample({ current, maxScroll: currentMaxScroll(), saved })
const adoptScrollPosition = (current: number) => {
lastSaved = current
lastHeight = el.scrollHeight
writeScrollPosition(key, current)
}
const stopRestore = () => {
if (restoreFrame) {
window.cancelAnimationFrame(restoreFrame)
@@ -97,14 +136,18 @@ export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
if (!restoring) return
restoring = false
stopRestore()
lastSaved = Math.round(el.scrollTop)
lastHeight = el.scrollHeight
writeScrollPosition(key, lastSaved)
const current = Math.round(el.scrollTop)
if (!sampleIsTrustworthy(current)) {
// 目标一直不可达(内容仍比记忆位置矮 / 还在加载):保留原记忆值,
// 不要把钳制出来的位置写回去。
return
}
adoptScrollPosition(current)
}
const tryRestore = () => {
if (!restoring) return
const max = Math.max(0, el.scrollHeight - el.clientHeight)
const max = currentMaxScroll()
const target = Math.min(saved, max)
if (target > 0 && Math.abs(el.scrollTop - target) > 1) {
el.scrollTop = target
@@ -125,8 +168,19 @@ export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
}
const saveNow = () => {
if (restoring) return
const current = Math.round(el.scrollTop)
// 内容还没长回来时读到的 scrollTop 不是用户选的位置,既不能写存储,
// 也不能当成“用户接管”。
if (!sampleIsTrustworthy(current)) return
if (restoring) {
if (Math.abs(current - saved) <= 1) return
// 恢复途中用户真的滚到了别处:交还控制权并记录这个位置,避免恢复
// 逻辑继续和用户抢滚动条。
restoring = false
stopRestore()
adoptScrollPosition(current)
return
}
const height = el.scrollHeight
if (
!shouldPersistScrollSample({
@@ -147,11 +201,13 @@ export function useScrollMemory(pathname: string, userKey = 'anonymous'): void {
const cancelRestore = () => {
if (!restoring) return
const current = Math.round(el.scrollTop)
// 恢复期间用户滚轮 / 触摸 / 按键只是误触或惯性(内容还没长高时他也确实
// 滚不动):继续完成恢复,并保留原来的记忆值。
if (!sampleIsTrustworthy(current)) return
restoring = false
stopRestore()
lastSaved = Math.round(el.scrollTop)
lastHeight = el.scrollHeight
writeScrollPosition(key, lastSaved)
adoptScrollPosition(current)
}
if (saved <= 0) {