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 > = { } ;
@@ -21,41 +20,23 @@ interface UseScrollRestoreOptions {
2120 timeout ?: number ;
2221 /** 是否启用调试日志 */
2322 debug ?: boolean ;
24- /** 内容稳定检测的等待时间(ms),默认 150 */
25- stabilityDelay ?: number ;
26- /** 是否在 KeepAlive 中使用 */
27- useKeepAlive ?: boolean ;
2823}
2924
3025const DEFAULT_OPTIONS : Required < Omit < UseScrollRestoreOptions , "isLoading" > > = {
3126 containerSelector : "main" ,
3227 timeout : 1500 ,
3328 debug : false ,
34- stabilityDelay : 0 ,
35- useKeepAlive : false ,
3629} ;
3730
3831/**
39- * 滚动位置还原 Hook (优化版)
40- *
41- * 特性:
42- * - 智能检测内容是否渲染完成(高度稳定检测)
43- * - 支持滚动到底部的场景
44- * - 避免滚动抖动和跳跃
45- * - 自动清理资源,防止内存泄漏
32+ * 滚动位置还原 Hook
33+ * 等页面数据加载完成后,用 requestAnimationFrame 等待内容高度足够再恢复。
4634 */
4735export function useScrollRestore (
4836 scrollPath : string ,
4937 options : UseScrollRestoreOptions = { } ,
5038) {
51- const {
52- containerSelector,
53- isLoading,
54- timeout,
55- debug,
56- stabilityDelay,
57- useKeepAlive,
58- } = {
39+ const { containerSelector, isLoading, timeout, debug } = {
5940 ...DEFAULT_OPTIONS ,
6041 ...options ,
6142 } ;
@@ -65,8 +46,6 @@ export function useScrollRestore(
6546 const cleanupRef = useRef < ( ( ) => void ) | null > ( null ) ;
6647 const settledRef = useRef ( false ) ;
6748 const lastPathRef = useRef < string > ( "" ) ;
68- const lastHeightRef = useRef ( 0 ) ;
69- const stabilityTimerRef = useRef < number | null > ( null ) ;
7049
7150 const log = useCallback (
7251 ( ...args : Parameters < Console [ "log" ] > ) => {
@@ -87,7 +66,6 @@ export function useScrollRestore(
8766 if ( lastPathRef . current !== location . pathname ) {
8867 settledRef . current = false ;
8968 lastPathRef . current = location . pathname ;
90- lastHeightRef . current = 0 ;
9169 }
9270
9371 // 清理上一次的副作用
@@ -126,28 +104,26 @@ export function useScrollRestore(
126104 return ;
127105 }
128106
129- let ro : ResizeObserver | null = null ;
107+ let frameId : number | null = null ;
130108 let fallbackTimer : number | null = null ;
109+ let cancelled = false ;
131110
132111 // 清理函数(先定义,避免在 performRestore 中引用未定义的变量)
133112 const cleanup = ( ) => {
134- if ( ro ) {
135- ro . disconnect ( ) ;
136- ro = null ;
113+ cancelled = true ;
114+ if ( frameId !== null ) {
115+ window . cancelAnimationFrame ( frameId ) ;
116+ frameId = null ;
137117 }
138118 if ( fallbackTimer !== null ) {
139119 window . clearTimeout ( fallbackTimer ) ;
140120 fallbackTimer = null ;
141121 }
142- if ( stabilityTimerRef . current !== null ) {
143- window . clearTimeout ( stabilityTimerRef . current ) ;
144- stabilityTimerRef . current = null ;
145- }
146122 } ;
147123
148124 // 执行滚动恢复
149125 const performRestore = ( reason : string ) => {
150- if ( settledRef . current ) return ;
126+ if ( settledRef . current || cancelled ) return ;
151127
152128 const maxScroll = Math . max (
153129 0 ,
@@ -172,67 +148,24 @@ export function useScrollRestore(
172148 cleanup ( ) ;
173149 } ;
174150
175- // 检查内容高度是否稳定
176- const checkStability = ( ) => {
177- const currentHeight = container . scrollHeight ;
178- const maxScroll = currentHeight - container . clientHeight ;
151+ const tryRestore = ( ) => {
152+ if ( settledRef . current || cancelled ) return ;
179153
180- log ( "Height check:" , {
181- current : currentHeight ,
182- last : lastHeightRef . current ,
183- maxScroll,
184- target,
185- } ) ;
154+ const maxScroll = Math . max (
155+ 0 ,
156+ container . scrollHeight - container . clientHeight ,
157+ ) ;
186158
187- // 情况1: 内容已经足够高,可以直接恢复
188159 if ( maxScroll >= target ) {
189160 performRestore ( "content sufficient" ) ;
190161 return ;
191162 }
192163
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 ) ;
164+ log ( "Content not high enough yet:" , { maxScroll, target } ) ;
165+ frameId = window . requestAnimationFrame ( tryRestore ) ;
219166 } ;
220167
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- }
168+ frameId = window . requestAnimationFrame ( tryRestore ) ;
236169
237170 // 超时保护
238171 fallbackTimer = window . setTimeout ( ( ) => {
@@ -250,41 +183,11 @@ export function useScrollRestore(
250183 isLoading ,
251184 containerSelector ,
252185 timeout ,
253- stabilityDelay ,
254186 log ,
255187 ] ) ;
256188
257189 // 普通模式:使用 useEffect
258190 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- } ) ;
191+ performScrollRestore ( ) ;
192+ } , [ performScrollRestore ] ) ;
290193}
0 commit comments