11import { useCallback , useEffect , useRef } from "react" ;
2- import { useActivate , useUnactivate } from "react-activation" ;
32import { useLocation } from "react-router-dom" ;
43
54const scrollPositions : Record < string , number > = { } ;
@@ -17,45 +16,24 @@ interface UseScrollRestoreOptions {
1716 containerSelector ?: string ;
1817 /** 是否正在加载中 */
1918 isLoading ?: boolean ;
20- /** 超时时间(ms),默认 2000 */
21- timeout ?: number ;
2219 /** 是否启用调试日志 */
2320 debug ?: boolean ;
24- /** 内容稳定检测的等待时间(ms),默认 150 */
25- stabilityDelay ?: number ;
26- /** 是否在 KeepAlive 中使用 */
27- useKeepAlive ?: boolean ;
2821}
2922
3023const DEFAULT_OPTIONS : Required < Omit < UseScrollRestoreOptions , "isLoading" > > = {
3124 containerSelector : "main" ,
32- timeout : 1500 ,
3325 debug : false ,
34- stabilityDelay : 0 ,
35- useKeepAlive : false ,
3626} ;
3727
3828/**
39- * 滚动位置还原 Hook (优化版)
40- *
41- * 特性:
42- * - 智能检测内容是否渲染完成(高度稳定检测)
43- * - 支持滚动到底部的场景
44- * - 避免滚动抖动和跳跃
45- * - 自动清理资源,防止内存泄漏
29+ * 滚动位置还原 Hook
30+ * 等页面数据加载完成后一帧恢复;目标超过当前高度时直接还原到当前底部。
4631 */
4732export function useScrollRestore (
4833 scrollPath : string ,
4934 options : UseScrollRestoreOptions = { } ,
5035) {
51- const {
52- containerSelector,
53- isLoading,
54- timeout,
55- debug,
56- stabilityDelay,
57- useKeepAlive,
58- } = {
36+ const { containerSelector, isLoading, debug } = {
5937 ...DEFAULT_OPTIONS ,
6038 ...options ,
6139 } ;
@@ -65,8 +43,6 @@ export function useScrollRestore(
6543 const cleanupRef = useRef < ( ( ) => void ) | null > ( null ) ;
6644 const settledRef = useRef ( false ) ;
6745 const lastPathRef = useRef < string > ( "" ) ;
68- const lastHeightRef = useRef ( 0 ) ;
69- const stabilityTimerRef = useRef < number | null > ( null ) ;
7046
7147 const log = useCallback (
7248 ( ...args : Parameters < Console [ "log" ] > ) => {
@@ -87,7 +63,6 @@ export function useScrollRestore(
8763 if ( lastPathRef . current !== location . pathname ) {
8864 settledRef . current = false ;
8965 lastPathRef . current = location . pathname ;
90- lastHeightRef . current = 0 ;
9166 }
9267
9368 // 清理上一次的副作用
@@ -126,28 +101,21 @@ export function useScrollRestore(
126101 return ;
127102 }
128103
129- let ro : ResizeObserver | null = null ;
130- let fallbackTimer : number | null = null ;
104+ let frameId : number | null = null ;
105+ let cancelled = false ;
131106
132107 // 清理函数(先定义,避免在 performRestore 中引用未定义的变量)
133108 const cleanup = ( ) => {
134- if ( ro ) {
135- ro . disconnect ( ) ;
136- ro = null ;
137- }
138- if ( fallbackTimer !== null ) {
139- window . clearTimeout ( fallbackTimer ) ;
140- fallbackTimer = null ;
141- }
142- if ( stabilityTimerRef . current !== null ) {
143- window . clearTimeout ( stabilityTimerRef . current ) ;
144- stabilityTimerRef . current = null ;
109+ cancelled = true ;
110+ if ( frameId !== null ) {
111+ window . cancelAnimationFrame ( frameId ) ;
112+ frameId = null ;
145113 }
146114 } ;
147115
148116 // 执行滚动恢复
149117 const performRestore = ( reason : string ) => {
150- if ( settledRef . current ) return ;
118+ if ( settledRef . current || cancelled ) return ;
151119
152120 const maxScroll = Math . max (
153121 0 ,
@@ -172,119 +140,19 @@ export function useScrollRestore(
172140 cleanup ( ) ;
173141 } ;
174142
175- // 检查内容高度是否稳定
176- const checkStability = ( ) => {
177- const currentHeight = container . scrollHeight ;
178- const maxScroll = currentHeight - container . clientHeight ;
179-
180- log ( "Height check:" , {
181- current : currentHeight ,
182- last : lastHeightRef . current ,
183- maxScroll,
184- target,
185- } ) ;
186-
187- // 情况1: 内容已经足够高,可以直接恢复
188- if ( maxScroll >= target ) {
189- performRestore ( "content sufficient" ) ;
190- return ;
191- }
192-
193- // 情况2: 高度稳定(不再增长)
194- if (
195- lastHeightRef . current > 0 &&
196- currentHeight === lastHeightRef . current
197- ) {
198- // 高度不再变化,说明内容已渲染完成
199- // 即使 maxScroll < target,也恢复到最大可滚动位置
200- performRestore ( "content stable" ) ;
201- return ;
202- }
203-
204- // 更新上次高度
205- lastHeightRef . current = currentHeight ;
206-
207- // 清除旧的稳定性计时器
208- if ( stabilityTimerRef . current !== null ) {
209- window . clearTimeout ( stabilityTimerRef . current ) ;
210- }
211-
212- // 设置新的稳定性计时器
213- // 如果在 stabilityDelay 时间内高度没有变化,认为内容已稳定
214- stabilityTimerRef . current = window . setTimeout ( ( ) => {
215- if ( ! settledRef . current ) {
216- checkStability ( ) ;
217- }
218- } , stabilityDelay ) ;
143+ const restoreNextFrame = ( ) => {
144+ if ( settledRef . current || cancelled ) return ;
145+ performRestore ( "next frame" ) ;
219146 } ;
220147
221- // 立即检查一次
222- checkStability ( ) ;
223-
224- // 使用 ResizeObserver 监听容器尺寸变化
225- try {
226- ro = new ResizeObserver ( ( ) => {
227- if ( ! settledRef . current ) {
228- checkStability ( ) ;
229- }
230- } ) ;
231- ro . observe ( container ) ;
232- log ( "ResizeObserver attached" ) ;
233- } catch ( err ) {
234- log ( "ResizeObserver not available" , err ) ;
235- }
236-
237- // 超时保护
238- fallbackTimer = window . setTimeout ( ( ) => {
239- if ( ! settledRef . current ) {
240- log ( "⏰ Timeout reached, forcing restore" ) ;
241- performRestore ( "timeout" ) ;
242- }
243- } , timeout ) ;
148+ frameId = window . requestAnimationFrame ( restoreNextFrame ) ;
244149
245150 cleanupRef . current = cleanup ;
246151 return cleanup ;
247- } , [
248- location . pathname ,
249- scrollPath ,
250- isLoading ,
251- containerSelector ,
252- timeout ,
253- stabilityDelay ,
254- log ,
255- ] ) ;
152+ } , [ location . pathname , scrollPath , isLoading , containerSelector , log ] ) ;
256153
257154 // 普通模式:使用 useEffect
258155 useEffect ( ( ) => {
259- if ( ! useKeepAlive ) {
260- performScrollRestore ( ) ;
261- }
262- } , [ useKeepAlive , performScrollRestore ] ) ;
263-
264- // KeepAlive 模式:使用 useActivate
265- useActivate ( ( ) => {
266- if ( useKeepAlive ) {
267- log ( "[KeepAlive] 组件激活,触发滚动恢复" ) ;
268- // 重置状态,因为可能是从其他页面返回
269- settledRef . current = false ;
270- lastHeightRef . current = 0 ;
271- performScrollRestore ( ) ;
272- }
273- } ) ;
274-
275- // KeepAlive 失活时清理
276- useUnactivate ( ( ) => {
277- if ( ! useKeepAlive ) return ;
278-
279- if ( cleanupRef . current ) {
280- cleanupRef . current ( ) ;
281- cleanupRef . current = null ;
282- }
283-
284- // KeepAlive 页面失活时将共享滚动容器归零,避免下一页继承旧滚动位置。
285- const container = document . querySelector < HTMLElement > ( containerSelector ) ;
286- if ( container ) {
287- container . scrollTop = 0 ;
288- }
289- } ) ;
156+ performScrollRestore ( ) ;
157+ } , [ performScrollRestore ] ) ;
290158}
0 commit comments