Skip to content

Commit 98f68c7

Browse files
committed
feat: keepalive
1 parent 651710b commit 98f68c7

File tree

1 file changed

+198
-27
lines changed

1 file changed

+198
-27
lines changed

src/components/KeepAlive/index.jsx

Lines changed: 198 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -312,13 +312,19 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
312312
}
313313

314314
setContainerNode(containerRef.current)
315+
if (process.env.NODE_ENV === 'development') {
316+
try {
317+
318+
console.debug('[KeepAlive] setContainerNode', id, containerRef.current)
319+
} catch (e) {}
320+
}
315321
// no cleanup here; mount/unmount handled elsewhere
316322
}, [id])
317323

318324
// Scroll restoration logic
319325
useEffect(() => {
320-
const container = containerRef.current
321-
if (!container) return
326+
const target = ActivityComponent ? placeholderRef.current : containerRef.current
327+
if (!target) return
322328

323329
const onScroll = (e) => {
324330
if (!active) return
@@ -329,13 +335,13 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
329335
}
330336

331337
// Capture scroll events to record positions
332-
container.addEventListener('scroll', onScroll, {
338+
target.addEventListener('scroll', onScroll, {
333339
capture: true,
334340
passive: true,
335341
})
336342

337343
return () => {
338-
container.removeEventListener('scroll', onScroll, { capture: true })
344+
target.removeEventListener('scroll', onScroll, { capture: true })
339345
}
340346
}, [active])
341347

@@ -370,40 +376,161 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
370376

371377
if (!container || !placeholder) return
372378

379+
// If React Activity API is available, avoid moving DOM to prevent forced reflows.
380+
// Render children inline into the placeholder and point container refs to placeholder.
381+
if (ActivityComponent) {
382+
try {
383+
containerRef.current = placeholder
384+
setContainerNode(placeholder)
385+
} catch (e) {}
386+
return
387+
}
388+
373389
// 如果 container 还在隐藏容器里(比如从缓存恢复),把它移到当前占位符下
374390
if (container.parentNode !== placeholder) {
375-
// 在移动DOM之前,发送自定义事件通知子组件
391+
// 在移动 DOM 之前,发送自定义事件通知子组件,然后同步移动到占位符下。
392+
// 之前使用短延迟的异步移动会导致渲染滞后和大量定时器/帧回调,移到同步移动以提升响应性。
376393
const event = new CustomEvent('keepalive-dom-move', {
377394
detail: { from: container.parentNode, to: placeholder },
378395
})
379-
container.dispatchEvent(event)
380396

381-
// 短暂延迟确保子组件有机会处理事件
382-
setTimeout(() => {
397+
let dispatchError = null
398+
try {
399+
container.dispatchEvent(event)
400+
} catch (e) {
401+
dispatchError = String(e && e.message)
402+
}
403+
404+
if (process.env.NODE_ENV === 'development') {
405+
try {
406+
407+
console.debug('[KeepAlive] appending container to placeholder', id, {
408+
from: container.parentNode,
409+
to: placeholder,
410+
dispatchError,
411+
})
412+
} catch (e) {}
413+
}
414+
415+
let appended = false
416+
try {
383417
if (container.parentNode !== placeholder) {
418+
container.dataset.keepaliveAttached = String(Date.now())
384419
placeholder.appendChild(container)
420+
appended = true
421+
422+
// Watchdog: if children not mounted soon after append, record and mark on body
423+
try {
424+
setTimeout(() => {
425+
try {
426+
if (container.childElementCount === 0) {
427+
if (typeof window !== 'undefined') {
428+
window.__keepalive_debug_details = window.__keepalive_debug_details || []
429+
window.__keepalive_debug_details.push({
430+
id,
431+
note: 'no-children-after-append',
432+
childElementCount: container.childElementCount,
433+
time: Date.now(),
434+
})
435+
try {
436+
document.body.dataset.keepaliveIssue = 'no-children'
437+
} catch (e) {}
438+
}
439+
}
440+
} catch (e) {}
441+
}, 50)
442+
} catch (e) {}
385443
}
386-
}, 0)
387-
}
444+
} catch (e) {
445+
// swallow; we'll record below
446+
}
388447

389-
// 如果使用 Activity,我们让 Activity 控制可见性,但我们需要确保 container 本身是 block
390-
// 并且我们仍然需要手动恢复滚动位置,因为 container 是我们手动管理的 DOM
391-
if (ActivityComponent) {
392-
container.style.display = 'block'
448+
// 记录附加信息,包含父节点信息与子元素计数,便于生产环境排查
449+
try {
450+
if (typeof window !== 'undefined') {
451+
window.__keepalive_debug_details = window.__keepalive_debug_details || []
452+
window.__keepalive_debug_details.push({
453+
id,
454+
parentTag: container.parentNode ? container.parentNode.tagName : null,
455+
parentClass: container.parentNode ? container.parentNode.className : null,
456+
childElementCount: container.childElementCount,
457+
appended,
458+
dispatchError,
459+
time: Date.now(),
460+
})
461+
}
462+
} catch (e) {}
463+
464+
// If we just appended and this instance is active, clear any stray inline "display: none" styles
465+
if (appended && active && shouldRender) {
466+
try {
467+
// run in next frame to let other sync updates finish
468+
requestAnimationFrame(() => {
469+
try {
470+
const cleared = []
471+
// Limit to direct descendants first for safety
472+
const nodes = Array.from(container.querySelectorAll('*'))
473+
nodes.forEach((el) => {
474+
try {
475+
if (el && el.style && el.style.display === 'none') {
476+
el.style.removeProperty('display')
477+
cleared.push(el.tagName)
478+
}
479+
} catch (e) {}
480+
})
481+
482+
// Ensure the root keepalive node is visible (force with !important)
483+
try {
484+
const root = container.querySelector(`[data-keepalive-id="${id}"]`)
485+
if (root && root.style) {
486+
root.style.setProperty('display', 'block', 'important')
487+
}
488+
} catch (e) {}
489+
490+
if (typeof window !== 'undefined') {
491+
window.__keepalive_debug_details = window.__keepalive_debug_details || []
492+
window.__keepalive_debug_details.push({
493+
id,
494+
note: 'cleared-inline-display-none',
495+
clearedCount: cleared.length,
496+
clearedTagsSample: cleared.slice(0, 5),
497+
time: Date.now(),
498+
})
499+
}
500+
} catch (e) {}
501+
})
502+
} catch (e) {}
503+
}
504+
505+
// Ensure root visible when active (force with !important) — covers cases where append didn't run
393506
if (active && shouldRender) {
394-
scrollPos.current.forEach((pos, node) => {
395-
if (node.isConnected) {
396-
node.scrollLeft = pos.left
397-
node.scrollTop = pos.top
507+
try {
508+
const root = container.querySelector(`[data-keepalive-id="${id}"]`)
509+
if (root && root.style) {
510+
root.style.setProperty('display', 'block', 'important')
511+
// also remove any stray inline display:none on descendants
512+
try {
513+
const nodes = Array.from(container.querySelectorAll('*'))
514+
nodes.forEach((el) => {
515+
try {
516+
if (el && el.style && el.style.display === 'none') {
517+
el.style.removeProperty('display')
518+
}
519+
} catch (e) {}
520+
})
521+
} catch (e) {}
522+
523+
if (typeof window !== 'undefined') {
524+
window.__keepalive_debug_details = window.__keepalive_debug_details || []
525+
window.__keepalive_debug_details.push({
526+
id,
527+
note: 'force-visible-root',
528+
time: Date.now(),
529+
})
530+
}
398531
}
399-
})
532+
} catch (e) {}
400533
}
401-
return
402-
}
403-
404-
if (active && shouldRender) {
405-
// 使用 CSS 切换可见性,性能远高于 appendChild
406-
container.style.display = 'block'
407534

408535
// Restore scroll positions
409536
scrollPos.current.forEach((pos, node) => {
@@ -418,14 +545,58 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
418545
}
419546
}, [active, shouldRender])
420547

548+
// Record lightweight runtime debug info in an effect (avoid doing impure work during render)
549+
useEffect(() => {
550+
if (typeof window === 'undefined') return
551+
if (!shouldRender) return
552+
try {
553+
window.__keepalive_debug = window.__keepalive_debug || []
554+
window.__keepalive_debug.push({
555+
id,
556+
active,
557+
shouldRender,
558+
containerNode: !!containerNode,
559+
childrenType: typeof children,
560+
time: Date.now(),
561+
})
562+
} catch (e) {}
563+
}, [shouldRender, active, containerNode, id, children])
564+
565+
// Activity mode: render children inline and avoid DOM moving to reduce reflows
566+
useEffect(() => {
567+
if (!ActivityComponent) return
568+
if (!active || !shouldRender) return
569+
try {
570+
requestAnimationFrame(() => {
571+
try {
572+
const p = placeholderRef.current
573+
if (!p) return
574+
const nodes = Array.from(p.querySelectorAll('*'))
575+
nodes.forEach((el) => {
576+
try {
577+
if (el && el.style && el.style.display === 'none') {
578+
el.style.removeProperty('display')
579+
}
580+
} catch (e) {}
581+
})
582+
if (typeof window !== 'undefined') {
583+
window.__keepalive_debug_details = window.__keepalive_debug_details || []
584+
window.__keepalive_debug_details.push({ id, note: 'activity-mode-cleaned-display', time: Date.now() })
585+
}
586+
} catch (e) {}
587+
})
588+
} catch (e) {}
589+
}, [ActivityComponent, active, isActivityVisible, shouldRender])
590+
421591
if (!shouldRender) return null
422592

423593
if (ActivityComponent) {
424594
return (
425595
<KeepAliveContext.Provider value={active}>
426596
<ActivityComponent mode={isActivityVisible ? 'visible' : 'hidden'}>
427-
<div ref={placeholderRef} style={{ width: '100%', height: '100%' }} />
428-
{containerNode && createPortal(children, containerNode)}
597+
<div ref={placeholderRef} style={{ width: '100%', height: '100%' }}>
598+
{children}
599+
</div>
429600
</ActivityComponent>
430601
</KeepAliveContext.Provider>
431602
)

0 commit comments

Comments
 (0)