一些關(guān)于react的keep-alive功能相關(guān)知識(shí)在這里(下)
作者:lulu_up
來源:SegmentFault 思否社區(qū)
本篇承接上篇內(nèi)部, 所以是從第九點(diǎn)開始
九、保留頁面scroll
比如頁面上的table里有100條數(shù)據(jù), 我們想看第100條數(shù)據(jù), 那就要滾動(dòng)不少距離, 不少場(chǎng)景這種滾動(dòng)距離也是有必要保留的。
這里使用的方法其實(shí)比較傳統(tǒng)啦, 首先在KeepAliveProvider 下發(fā)一個(gè)處理滾動(dòng)的方法:
const handleScroll = useCallback(
(cacheId, event) => {
if (catheStates?.[cacheId]) {
const target = event.target
const scrolls = catheStates[cacheId].scrolls
scrolls[target] = target.scrollTop
}
},
[catheStates]
)
在Keeper組件里面接收并執(zhí)行:
const { dispatch, mount, handleScroll } = useContext(CacheContext)
useEffect(() => {
const onScroll = handleScroll.bind(null, cacheId)
(divRef?.current as any)?.addEventListener?.('scroll', onScroll, true)
return (divRef?.current as any)?.addEventListener?.('scroll', onScroll, true)
}, [handleScroll])
在Keeper里面將滾動(dòng)屬性賦予元素:
useEffect(() => {
const catheState = catheStates[cacheId]
if (catheState && catheState.doms) {
const doms = catheState.doms
doms.forEach((dom: any) => {
(divRef?.current as any)?.appendChild?.(dom)
})
// 新增
doms.forEach((dom: any) => {
if (catheState.scrolls[dom]) {
dom.scrollTop = catheState.scrolls[dom]
}
})
} else {
mount({
cacheId,
reactElement: props.children
})
}
}, [catheStates])
這里如果不主動(dòng)增加賦予scroll的方法的話, 滾動(dòng)距離是不會(huì)被保存的, 因?yàn)镵eeper每次都是新的。
十、KeepAliveProvider內(nèi)部 Keeper子組件內(nèi)部的CacheContext
我們是把組件渲染在 KeepAliveProvider 里面, 那么如果某個(gè)Provider是在 KeepAliveProvider 內(nèi)部定義的, 則KeepAliveProvider級(jí)別的組件是無法使用 Consumer 拿到這個(gè)值的。
<Keeper
cacheId="home"
context={{ Provider: Provider, value: value }}>
<Home />
</Keeper>
我們拿到這兩個(gè)值后直接在Keeper中修改reactElement的結(jié)構(gòu):
mount({
cacheId,
reactElement: context ?
<context.Provider
value={context.value}>{props.children}</context.Provider> :
props.children
})
十一、需要傳值的組件
大家有沒有發(fā)現(xiàn)上述組件所有邏輯, 都是直接寫在Keeper標(biāo)簽里面的, 并沒有任何的傳值, 但是比較常見的一種場(chǎng)景是下面這樣的:
function Root (){
const [n, setN] = useState(1)
return
(
<>
<button onClick={()=>setN(n+1)}>n+1</button>
<Keeper>
<Home n={n} />
</Keeper>
</>
)
}
這個(gè)n是Keeper外層傳遞給Home組件的, 這種寫法下會(huì)導(dǎo)致n雖然變化了但是Home里面不會(huì)響應(yīng)。
這個(gè)bug我是這樣發(fā)現(xiàn)的, 當(dāng)我把這個(gè)插件用在我們團(tuán)隊(duì)的項(xiàng)目里的一個(gè)表格為主的頁面時(shí) , table一直顯示是空的, 并且輸入框也無法輸入值, 經(jīng)過測(cè)試發(fā)現(xiàn)其實(shí)值是有變化的, 只是沒有展示在組件的dom上。
嘗試了好久后試了下react-activation 很遺憾它也有相同的問題, 那其實(shí)就說明這個(gè)bug很可能無法解決或者就是這個(gè)插件本身的架構(gòu)存在的問題。
十二、為何這么奇怪的bug場(chǎng)景
當(dāng)時(shí)這個(gè)bug折磨了我一天半的時(shí)間, 最后定位到外界的傳參已經(jīng)不能算是這個(gè)組件本身的參數(shù)了, 我們組件的實(shí)際渲染位置是 KeepAliveProvider 的第一層, 而Keeper的外層還在KeepAliveProvider的更內(nèi)層, 這就導(dǎo)致這些值的變化其實(shí)是沒有能夠影響到組件。
可以理解為這些值的變化, 比如n的變化就如同window.n的改變一樣, react組件是不會(huì)去響應(yīng)這個(gè)變化的。
那其實(shí)我們要做的就是讓外層傳入的值的變化, 可以帶動(dòng)組件的樣式變化 (逐漸入坑!)。
十三、將props單獨(dú)拿出來
我借鑒了網(wǎng)上另一種keep-alive組件的寫法, 把Keeper組件改為一個(gè)keeper的方法, 這個(gè)方法返回一個(gè)組件看, 這樣就可以接收一個(gè)props了, 也就把變量圈定在props這個(gè)范圍:
const Home = keeper(HomePage, { cacheId: 'home' })
function Root(){
const [n, setN] = useState(1)
return (
<>
<button onClick={()=>setN(n+1)}>n+1</button>
<Home n={n}> // 此處可以傳值了
</>
)
}
這樣做的目的是讓開發(fā)者把能夠影響組件狀態(tài)的參數(shù)一口氣傳進(jìn)來, 比如之前一個(gè)Keeper里面可以有多個(gè)組件, 這種情況就不好控制哪些參數(shù)變化會(huì)導(dǎo)致哪些組件更新, 但以組件的方式可以明顯得知組件接收到的props里面的值的改變會(huì)導(dǎo)致組件更新。
我想到的方案是, 在KeepAliveProvider里面新建propsObj, 用來專門儲(chǔ)存每個(gè)緩存組件的props, 之所以如此設(shè)計(jì)將其單獨(dú)拿出來, 是要把傳參與組件的邏輯拆分開, 不少邏輯會(huì)監(jiān)控catheStates的變化而執(zhí)行, 但是props的變化沒有必要觸發(fā)這些。
const [propsObj, setPropsObj] = useState<any>();
return (
<CacheContext.Provider value={{ setPropsObj, propsObj }}>
{props.children}
//.... 略
KeepAliveProvider 里面的渲染需要變一個(gè)形式, reactElement 變成組件了, 別忘了名字要變成大寫的。
// 舊的
// {reactElement}
// 新的
{propsObj &&
<ReactElement {...propsObj[cacheId]}></ReactElement>}
改裝一下Keeper文件, 首先要把文件名改為 keeper, 導(dǎo)出的方法要進(jìn)行一下更改。
export default function (
RealComponent: React.FunctionComponent<any>, { cacheId = '' }) {
return function Keeper(props: any) {
// ... 略
Keeper內(nèi)mount方法的使用也稍作調(diào)整:
mount({
cacheId,
ReactElement: RealComponent
})
關(guān)鍵的來了, 我們要在Keeper里面監(jiān)測(cè)props的變化, 來更新propsObj:
const { propsObj, setPropsObj } = useContext(CacheContext)
useEffect(() => {
setPropsObj({
...propsObj,
[cacheId]: props
})
}, [props])
十四、緩存失敗的bug
上述我們已經(jīng)把插件改裝了形式, 并且發(fā)現(xiàn)可以讓如下場(chǎng)景正常渲染, Home組件的props是外界傳入的:
const Home = keeper(HomePage, { cacheId: 'home' })
const RootComponent: React.FC = () => {
return (
<KeepAliveProvider>
<Router>
<Routes>
<Route path={'/'} element={<Mid />} />
</Routes>
</Router>
</KeepAliveProvider>
)
}
function Mid() {
const [n, setN] = useState(1)
return (
<div>
<button onClick={() => setN(n + 1)}>n+1</button>
<Home n={n}></Home>
</div>
)
}
function HomePage(props: { n: number }) {
return <div>home {props.n}</div>
}
但是此時(shí)如果切換頁面后再返回home頁面, home頁面的緩存是會(huì)失效的。
其實(shí)是因?yàn)槲覀儗?shí)時(shí)監(jiān)控props的變化, 下次重新渲染時(shí)會(huì)導(dǎo)致props變化, 然后值就會(huì)被初始化了, 導(dǎo)致組件也恢復(fù)到了早期的配置, 可是.... 這不就是緩存失敗了嗎?
每次組件props被重置就會(huì)導(dǎo)致組件的相關(guān)數(shù)據(jù)被重置, 嘗試把home組件做如下更改:
function HomePage(props: { n: number }) {
const [x, setX] = useState(1)
return (
<div>
<button onClick={() => setX(x + 1)}>x + 1</button>
<div>home {props.n}</div>
<div>home: x {x}</div>
</div>
)
}
上述寫法會(huì)導(dǎo)致每次激活home組件, 只能保留x的值, n的值會(huì)與傳入的相同。
這種變化可能會(huì)導(dǎo)致bug, 假設(shè)只有 n > 2 才能讓 x > 3, 此時(shí)我們通過點(diǎn)擊事件讓 n = 5 , x = 4了, 此時(shí)切換到其他頁面再回來, 就變成了n = 1, x=4, 違背了我們的初始限制條件, 以此類推在真實(shí)復(fù)雜的開發(fā)環(huán)境中此現(xiàn)象會(huì)導(dǎo)致各種奇怪的問題。
十五、認(rèn)知的代價(jià)
上面的場(chǎng)景可以通過開發(fā)人員自己來控制, 理想情況是keep-alive插件只用來處理不需要外界傳參, 以及不會(huì)被外界參數(shù)的變化影響的組件, 但這就開始麻煩了。
這類問題導(dǎo)致開發(fā)者在插件身上要花的學(xué)習(xí)成本提高, 使用成本提高, 并且如果某個(gè)組件本來不需要傳參, 我們用keep-alive包裹起來了, 后續(xù)又需要傳參了, 改變的成本想想都麻煩。
網(wǎng)上現(xiàn)有(2022年04月10日17:16:22)組件的官網(wǎng)基本是沒有認(rèn)真的對(duì)用戶講述相關(guān)的問題, 往往都是以介紹"使用方法"與闡述自己的優(yōu)勢(shì)為主, 這就導(dǎo)致用戶被莫名其妙的bug折磨。
傳遞 Provider 的方法也有問題, 需要傳遞可能不是本頁代碼的Provider, 難受的了啊。
想要解決keep-alive相關(guān)問題的思路可以換一下, 最好是在react源碼里支持一波, 比如可以指定某些組件不被銷毀, 其實(shí)我們可以關(guān)注一下react18的后續(xù)版本, 現(xiàn)在這個(gè)時(shí)間段react18發(fā)布了正式版。
十六、如何升級(jí)到react18
方式一: create-react-app 創(chuàng)建新項(xiàng)目
現(xiàn)階段直接使用下面的命令, 就可創(chuàng)建react18項(xiàng)目:
npx create-react-app my_react

下面這種使用 --template 指定模板的還不行, 因?yàn)槟0宕a還沒更新:
npx create-react-app my_react --template typescript
方式二: 老項(xiàng)目改裝
首先直接把依賴?yán)锩娴膔eact 與 react-dom的版本號(hào)改成 "^18.0.0"即可。
兩種方式都需要修改 index.js
啟動(dòng)項(xiàng)目會(huì)有報(bào)錯(cuò)信息:

舊版的index.js

新版的index.js

其他的沒有太多更改了。
十七、react18 Offscreen 組件的用法
Offscreen 允許 React 通過隱藏組件而不是卸載組件來保持這樣的狀態(tài), React 將調(diào)用與卸載時(shí)相同的生命周期鉤子, 但它也會(huì)保留 React 組件和 DOM 元素的狀態(tài)。
React Activation 中也推薦大家關(guān)注這個(gè)屬性:

Offscreen 是什么的官方說法可以看這篇文章里的翻譯: https://www.jianshu.com/p/184d981b8743

Offscreen的測(cè)試用例:

遺憾的是 Offscreen 組件并沒有在當(dāng)前版本推出, 其還處于不穩(wěn)定階段, 但我們可以通過 react18 里面的測(cè)試用例來預(yù)覽一下其用法:

通過上述寫法還無法看出 Offscreen 到底如何使用, 只知道它可能是以組件的形式出現(xiàn), 并且需要傳入一個(gè)mode屬性, 更多用法期待官方盡快推出吧。
讓我們一起期待 react18 來解決keep-alive這個(gè)問題吧, 這次就是這樣, 希望與你一起進(jìn)步。

