Open MatrixAge opened 1 year ago
通过这种方式或者ahooks的useInViewport来人为修复这个问题也不失为一个解决
主要是考虑到这个库的实现更接近于可能的官方实现,而不是像react- activation那种重度hack,有一点小瑕疵,只要能通过其他方式来弥补,也没啥问题。
实现思路还是很值得学习的,加油.
display: none
应该是你某个组件,mount后加上的,unmount的时候没有还原,可以排查一下
也可能是第三方的UI库造成的副作用
本库的实现原理,只操作了 react fiber 和 dom 的装/卸载。并未涉及到样式操作
上述问题是在umi4 react18 react-router6下表现的,今天在umi3上测试了一下(umi3用的是react17.x react-router5),切换页面虽然没有display: none,但有别的问题:
在进行路由切换的时候,会导致这个组件已卸载但还是执行setV导致的报错,当页面且回来之后,fiber节点可能还记录了卸载之后的setV操作(在已经切换之后记录的),这就导致还原的fiber节点和实际fiber不一致,导致setV反复执行,5,6,5,6这种结果.
这种边缘情况,以上,虽然在umi3中,针对这种情况也可以用ref或者变量截断的方式来避免出现。
用来复现的代码
import { useEffect, useState } from 'react'
import { keepAlive } from 'react-fiber-keep-alive'
const Index = () => {
const [v, setV] = useState(0);
console.log(v);
useEffect(() => {
const timer = setInterval(() => setV(v + 1), 1000);
return () => {
clearInterval(timer);
console.log('out');
};
}, [v]);
return <div>{v}</div>;
};
export default keepAlive(Index, () => 'A');
观察了一下,发现在umi3中出现上述问题的原因可能是useEffect在组件卸载时,KeepAlive拿这个组件的fiber信息存入内存,然后卸载,但是fiber信息在存入内存之后还执行了一次timer,但是这个时候组件其实unmoumted了,就会报这个错。
不理解的是(相关逻辑细节),为什么还原的时候会出现反复setV。
发现了华点,来回切换路由之后,竟然出现了切到别的页面,定时器未卸载,还一直在执行的情况:
import { useEffect, useState } from 'react'
import { keepAlive } from 'react-fiber-keep-alive'
const Index = () => {
const [v, setV] = useState(0);
console.log(v);
useEffect(() => {
let is_mounted = true;
const timer = setInterval(() => {
if (is_mounted) {
console.log('setV');
setV(v + 1);
}
}, 1000);
return () => {
is_mounted = false;
clearInterval(timer);
};
}, [v]);
return <div>{v}</div>;
};
export default keepAlive(Index, () => 'A');
经过测试,上面这个问题不会在umi4 react18.1 react-router6项目中出现 (出现的是那个display: none) 的问题。
我用单纯的 react 17.0.2 / 18.1.0 + react-router 5.2.0 + react-fiber-keep-alive 0.7.1 和你的代码 并未复现你的问题,能否放一个 demo 到 https://codesandbox.io/ 我没用过 umi,不清楚是不是 umi 带来的副作用
umi 4 因该是使用了 React-18 的 <Offscreen>
组件
会执行 hideInstance() 操作加上 display: none
https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMHostConfig.js#L636
抱歉,在 react 17.0.2 上复现了
codesanbox真鸡儿难用,搞了半天写一点代码编译半天,换github demo了:
https://github.com/MatrixAge/codesandbox
总体上umi4用keepAlive是能用的,解决那个display: none就行了,umi3上是完全不可用的。
umi 4 因该是使用了 React-18 的
<Offscreen>
组件 会执行 hideInstance() 操作加上display: none
https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMHostConfig.js#L636
与umi4 和 react-router都没关系,是react18自己执行的hideInstance( instance ):
这个方法竟然是react-reconciler提供的,感觉可以直接引入react-reconciler,如果获取到是react18,使用react-reconciler的hideInstance等方法去维护需要被keepAlive的节点。
看了一下官方的代码,他们正在实现这部分的功能,说不定在18.3就能看到了。
介于 react 17 的执行顺序问题,有一个阶段keep-alive无法介入其中。
可以使用 setTimeout
代替 setInterval
,解决你的问题。
搞了一个hooks用来弥补一些不足,针对react18+场景下的可使用方案:
import { useScroll } from 'ahooks'
import { useEffect, useRef } from 'react'
import { markEffectHookIsOnetime } from 'react-fiber-keep-alive'
export default () => {
const el = useRef<HTMLDivElement>(null)
const scroll = useScroll(el)
useEffect(() => {
if (!el.current) return
if (el.current.style.display !== 'none') return
el.current.style.setProperty('display', 'flex')
}, [])
useEffect(
markEffectHookIsOnetime(() => {
if (!scroll?.top) return
el.current!.scrollTop = scroll.top
})
)
return el
}
针对需要keepAlive的元素,直接绑定ref即可:
import { keepAlive } from 'react-fiber-keep-alive'
import data from '@/_Data_/treeview'
import { TreeView } from '@/components'
import { useKeepAlive } from '@/hooks'
const Index = () => {
const page = useKeepAlive()
return (
<div className='_keepalive w_100 border_box flex flex_column' ref={page}>
<TreeView data={data} />
</div>
)
}
export default keepAlive(Index, () => 'models')
由于这里只保存keepAlive目标容器的滚动进度,所以需要仅允许目标容器进行滚动:
._keepalive {
height: 100%;
overflow-y: scroll;
}
一般不用刻意手动保留 scroll 位置
理论上是这样 但是react18加了 display: none,导致必须在切换回来时手动设置display: flex 来让dom显示,而这个操作会导致scrollTop变成0,所以就有恢复scrollTop的操作。
都是连带的问题。