React 中的狀態自動保存(KeepAlive)

blank

React 中的狀態自動保存(KeepAlive)

什麼是狀態保存?

假設有下述場景:

移動端中,用戶訪問了一個列表頁,上拉瀏覽列表頁的過程中,隨著滾動高度逐漸增加,數據也將採用觸底分頁加載的形式逐步增加,列表頁瀏覽到某個位置,用戶看到了感興趣的項目,點擊查看其詳情,進入詳情頁,從詳情頁退回列表頁時,需要停留在離開列表頁時的瀏覽位置上

類似的數據或場景還有已填寫但未提交的表單管理系統中可切換和可關閉的功能標籤等,這類數據隨著用戶交互逐漸變化或增長,這裡理解為狀態,在交互過程中,因為某些原因需要臨時離開交互場景,則需要對狀態進行保存

React中,我們通常會使用路由去管理不同的頁面,而在切換頁面時,路由將會卸載掉未匹配的頁面組件,所以上述列表頁例子中,當用戶從詳情頁退回列表頁時,會回到列表頁頂部,因為列表頁組件被路由卸載後重建了,狀態被丟失

如何實現React 中的狀態保存

Vue中,我們可以非常便捷地通過<keep-alive>標籤實現狀態的保存,該標籤會緩存不活動的組件實例,而不是銷毀它們

而在React中並沒有這個功能,曾經有人在官方提過功能issues ,但官方認為這個功能容易造成內存洩露,表示暫時不考慮支持,所以我們需要自己想辦法了

常見的解決方式:手動保存狀態

手動保存狀態,是比較常見的解決方式,可以配合React組件的componentWillUnmount生命週期通過redux之類的狀態管理層對數據進行保存,通過componentDidMount週期進行數據恢復

在需要保存的狀態較少時,這種方式可以比較快地實現我們所需功能,但在數據量大或者情況多變時,手動保存狀態就會變成一件麻煩事了

作為程序員,當然是盡可能懶啦,為了不需要每次都關心如何對數據進行保存恢復,我們需要研究如何自動保存狀態

通過路由實現自動狀態保存(通常使用react-router

既然React中狀態的丟失是由於路由切換時卸載了組件引起的,那可以嘗試從路由機制上去入手,改變路由對組件的渲染行為

我們有以下的方式去實現這個功能

  1. 重寫<Route>組件,可參考react-live-route

    重寫可以實現我們想要的功能,但成本也比較高,需要注意對原始<Route>功能的保存,以及多個react-router版本的兼容

  2. 重寫路由庫,可參考react-keeper

    重寫路由庫成本是一般開發者無法承受的,且完全替換掉路由方案是一個風險較大的事情,需要較為慎重地考慮

  3. 基於<Route>組件現有行為做拓展,可參考react-router-cache-route

    在閱讀了<Route>的源碼後發現,如果使用component或者render屬性,都無法避免路由在不匹配時被卸載掉的命運

    但將children屬性當作方法來使用,我們就有手動控制渲染的行為的可能,關鍵代碼在此處

    github.com/ReactTrainin

//節選自Route組件中的render函數if(typeofchildren==="function"){children=children(props);// children是函數時,將對children進行調用得到真實的渲染結果if(children===undefined){...children=null;}}return(<RouterContext.Providervalue={props}>{children&&!isEmptyChildren(children)?children// children存在時,將使用children進行渲染:props.match?component?React.createElement(component,props):render?render(props):null//使用render屬性無法阻止組件的卸載:null//使用component屬性無法阻止組件的卸載}</RouterContext.Provider>);

基於上述源碼探究,我們可以對<Route>進行拓展,將<Route>的不匹配行為由卸載調整為隱藏,如下

<Routeexactpath="/list">{props=>(<divstyle={props.match?null:{display:'none'}}><List{...props}/></div>)}</Route>

上述是最簡的調整方式,實際情況中也需要考慮隱藏狀態下matchnull導致組件報錯的問題,且由於不再是組件卸載,所以和TransitionGroup配合得不好,導致轉場動畫難以實現

使用react-router-cache-route ,得到的效果大致如下圖,

blank

上述探究了通過路由入手實現自動狀態保存的可能,以及現有的實現,但終究不是真實的、純粹的KeepAlive功能,接下來我們嘗試探究真實KeepAlive功能的實現

模擬真實的<KeepAlive>功能

以下是期望的使用方式

functionApp(){const[show,setShow]=useState(true)return(<div><buttononClick={()=>setShow(show=>!show)}>Toggle</button>{show&&(<KeepAlive><Test/></KeepAlive>)}</div>)}

實現原理說起來較為簡單,由於React會卸載掉處於固有組件層級內的組件,所以我們需要將<KeepAlive>中的組件,也就是其children屬性抽取出來,渲染到一個不會被卸載的組件<Keeper>內,再使用DOM操作將<Keeper>內的真實內容移入對應<KeepAlive> ,就可以實現此功能

這裡做了一個最簡的、不到70行代碼的<KeepAlive />實現範例

以下是react-activation的實現效果

blank

下圖為<KeepAlive>的實現原理說明

blank

實際實現過程中,遇到了許多問題,都是由於打破了原有React層級關係引起的,例如

  • 渲染延遲( react-activation中已修復)
  • Context上下文功能失效( react-activation中已修復)
  • Error Boundaries失效( react-activation中已修復)
  • React.Suspense & React.lazy失效( react-activation中已修復)
  • React 合成事件冒泡失效
  • 其他未發現的功能

但上述問題,大多數是可以通過橋接機制修復的,具體可以參考此處issues

相同的、更早的實現還有react-keep-alive

結語

狀態緩存是應用中十分常見的需求,在需要處理的數據量較少時,使用手動狀態緩存就可以解決大多數問題,但當情況復雜時,還需要嘗試將緩存功能單獨拎出來解決,以便在業務開發過程中更好地進行關注點分離

目前的實現都有各自的問題,但其探究過程十分有趣,最好的方式仍是官方的支持,但目前還不能報太大期望

What do you think?

Written by marketer

blank

如何保證前端項目代碼質量

blank

10分鐘徹底搞懂前端頁面性能監控