()
| 98 | * PageEditor 的布局,改其滚动结构时需复核。 |
| 99 | */ |
| 100 | export function ScrollMemory() { |
| 101 | const pathname = usePathname(); |
| 102 | const searchParams = useSearchParams(); |
| 103 | |
| 104 | const keyRef = useRef<string>(""); // 当前历史条目 key(滚动监听据此保存) |
| 105 | const lastDecidedKeyRef = useRef<string>(""); // 去重:同一条目只决策一次 |
| 106 | const lastDecidedHashRef = useRef<string>(""); // 上次决策时的 location.hash(识别克隆条目) |
| 107 | const decidedHrefRef = useRef<string>(""); // 上次决策时的 pathname+search(用于分流) |
| 108 | const isRestoringRef = useRef(false); // 还原期间抑制回存,防夹断中间态写脏 |
| 109 | const cancelRestoreRef = useRef<(() => void) | null>(null); |
| 110 | const runDecisionRef = useRef<() => void>(() => {}); |
| 111 | |
| 112 | // 导航决策:定位当前历史条目 → 有记忆则还原;否则置顶 / 让位给 hash。 |
| 113 | // 不读 pathname,直接读 history.state 的 key,故任何触发源都能复用。 |
| 114 | // 在每次 render 赋值(读的全是 ref,无闭包陈旧问题),供下面三个监听调用。 |
| 115 | runDecisionRef.current = () => { |
| 116 | let key = currentEntryKey(); |
| 117 | const hash = window.location.hash; |
| 118 | // 去重 vs 识别克隆条目:浏览器对原生 location.hash 跳转(如无 clipboard 时点 §N 自锚 |
| 119 | // 走的 `window.location.hash = …` fallback)会**克隆**当前条目的 history.state,连 |
| 120 | // __kbScrollKey 一起复制 → 新条目与原条目共用一个 key。 |
| 121 | // - key 相同且 hash 也相同 → 真·同条目重复触发 → 去重 return(原行为)。 |
| 122 | // - key 相同但 hash 变了 → 这是克隆出来的新条目 → 给它重发独立 key,使其有自己的 |
| 123 | // scrollTop 槽位:避免原条目阅读位置被锚点位置覆盖、且后退回原条目时还原不被误挡。 |
| 124 | if (key === lastDecidedKeyRef.current) { |
| 125 | if (hash === lastDecidedHashRef.current) return; |
| 126 | key = forceNewEntryKey(); |
| 127 | } |
| 128 | const url = currentHref(); |
| 129 | lastDecidedKeyRef.current = key; |
| 130 | lastDecidedHashRef.current = hash; |
| 131 | keyRef.current = key; |
| 132 | decidedHrefRef.current = url; |
| 133 | |
| 134 | cancelRestoreRef.current?.(); // 取消在途还原(会解除 isRestoring) |
| 135 | cancelRestoreRef.current = null; |
| 136 | clearScrollClaim(); // 丢掉上一次导航残留的接管,避免误让位本次(尤其全新锚点导航) |
| 137 | |
| 138 | const container = findContainer(); |
| 139 | if (!container) return; |
| 140 | |
| 141 | // 优先级:① 精确 entry-key 命中(后退/前进回到本条目)→ 无条件还原,最贴合浏览器 |
| 142 | // 原生语义,即便带 hash 也还原到离开时的位置;② 否则若本次导航带显式 hash(点引用/ |
| 143 | // 锚点链接)→ 让位给 HashScroller 跳锚点,不被 URL 兜底还原盖掉(显式锚点是明确意图, |
| 144 | // 优先于"这个页面上次读到哪");③ 无 hash 时才用 URL 兜底还原曾读过的页面。 |
| 145 | const byEntry = getSavedScrollByEntry(key); |
| 146 | const byUrl = window.location.hash ? undefined : getSavedScrollByUrl(url); |
| 147 | const saved = byEntry ?? byUrl; |
| 148 | if (saved != null) { |
| 149 | // 还原阅读位置:接管让 HashScroller 让位(否则它会把页面又拽到锚点,盖掉真实阅读位置)。 |
| 150 | const token = holdScrollControl(); |
| 151 | isRestoringRef.current = true; |
| 152 | cancelRestoreRef.current = restoreWhenStable(container, saved, () => { |
| 153 | isRestoringRef.current = false; |
| 154 | cancelRestoreRef.current = null; |
| 155 | // 还原结束后延后释放接管(盖过 HashScroller 同布局稳定点的 tryScroll)。 |
| 156 | window.setTimeout(() => releaseScrollControl(token), RELEASE_DELAY_MS); |
| 157 | }); |
nothing calls this directly
no test coverage detected