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

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

Offscreen 是什么的官方說法可以看這篇文章里的翻譯: React v18.0新特性官方文檔[中英文對照

Offscreen的測試用例:

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

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

往期推薦



最后
歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...


