圖文無關javascript
假設有下述場景:vue
移動端中,用戶訪問了一個列表頁,上拉瀏覽列表頁的過程當中,隨着滾動高度逐漸增長,數據也將採用觸底分頁加載的形式逐步增長,列表頁瀏覽到某個位置,用戶看到了感興趣的項目,點擊查看其詳情,進入詳情頁,從詳情頁退回列表頁時,須要停留在離開列表頁時的瀏覽位置上java
相似的數據或場景還有已填寫但未提交的表單、管理系統中可切換和可關閉的功能標籤等,這類數據隨着用戶交互逐漸變化或增加,這裏理解爲狀態,在交互過程當中,由於某些緣由須要臨時離開交互場景,則須要對狀態進行保存react
在 React
中,咱們一般會使用路由去管理不一樣的頁面,而在切換頁面時,路由將會卸載掉未匹配的頁面組件,因此上述列表頁例子中,當用戶從詳情頁退回列表頁時,會回到列表頁頂部,由於列表頁組件被路由卸載後重建了,狀態被丟失git
在 Vue
中,咱們能夠很是便捷地經過 <keep-alive>
標籤實現狀態的保存,該標籤會緩存不活動的組件實例,而不是銷燬它們程序員
而在 React
中並無這個功能,曾經有人在官方提過功能 issues ,但官方認爲這個功能容易形成內存泄露,表示暫時不考慮支持,因此咱們須要本身想辦法了github
手動保存狀態,是比較常見的解決方式,能夠配合 React
組件的 componentWillUnmount
生命週期經過 redux
之類的狀態管理層對數據進行保存,經過 componentDidMount
週期進行數據恢復redux
在須要保存的狀態較少時,這種方式能夠比較快地實現咱們所需功能,但在數據量大或者狀況多變時,手動保存狀態就會變成一件麻煩事了api
做爲程序員,固然是儘量懶啦,爲了避免須要每次都關心如何對數據進行保存恢復,咱們須要研究如何自動保存狀態緩存
既然 React 中狀態的丟失是因爲路由切換時卸載了組件引發的,那能夠嘗試從路由機制上去入手,改變路由對組件的渲染行爲
咱們有如下的方式去實現這個功能
<Route>
組件,可參考 react-live-route重寫能夠實現咱們想要的功能,但成本也比較高,須要注意對原始 <Route>
功能的保存,以及多個 react-router
版本的兼容
重寫路由庫成本是通常開發者沒法承受的,且徹底替換掉路由方案是一個風險較大的事情,須要較爲慎重地考慮
基於 <Route>
組件現有行爲作拓展,可參考 react-router-cache-route
在閱讀了 <Route>
的源碼後發現,若是使用 component
或者 render
屬性,都沒法避免路由在不匹配時被卸載掉的命運
但將 children
屬性看成方法來使用,咱們就有手動控制渲染的行爲的可能,關鍵代碼在此處
https://github.com/ReactTrain...
// 節選自 Route 組件中的 render 函數 if (typeof children === "function") { children = children(props); // children 是函數時,將對 children 進行調用獲得真實的渲染結果 if (children === undefined) { ... children = null; } } return ( <RouterContext.Provider value={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>
的不匹配行爲由卸載調整爲隱藏,以下
<Route exact path="/list"> {props => ( <div style={props.match ? null : { display: 'none' }}> <List {...props} /> </div> )} </Route>
上述是最簡的調整方式,實際狀況中也須要考慮隱藏狀態下 match
爲 null
致使組件報錯的問題,且因爲再也不是組件卸載,因此和 TransitionGroup
配合得很差,致使轉場動畫難以實現
使用 react-router-cache-route,獲得的效果大體以下圖,
上述探究了經過路由入手實現自動狀態保存的可能,以及現有的實現,但終究不是真實的、純粹的 KeepAlive
功能,接下來咱們嘗試探究真實 KeepAlive
功能的實現
<KeepAlive>
功能如下是指望的使用方式
function App() { const [show, setShow] = useState(true) return ( <div> <button onClick={() => setShow(show => !show)}>Toggle</button> {show && ( <KeepAlive> <Test /> </KeepAlive> )} </div> ) }
實現原理提及來較爲簡單,因爲 React
會卸載掉處於固有組件層級內的組件,因此咱們須要將 <KeepAlive>
中的組件,也就是其 children
屬性抽取出來,渲染到一個不會被卸載的組件 <Keeper>
內,再使用 DOM
操做將 <Keeper>
內的真實內容移入對應 <KeepAlive>
,就能夠實現此功能
這裏作了一個最簡的、不到 70 行的 <KeepAlive>
實現示例:最簡實現
如下是 react-activation 的實現效果
下圖爲 <KeepAlive>
的實現原理說明
實際實現過程當中,遇到了許多問題,都是因爲打破了原有 React
層級關係引發的,例如
react-activation
中已修復)Context
上下文功能失效(react-activation
中已修復)Error Boundaries
失效(react-activation
中已修復)React.Suspense
& React.lazy
失效(react-activation
中已修復)但上述問題,大多數是能夠經過橋接機制修復的,具體能夠參考此處 issues
相同的、更早的實現還有 react-keep-alive
狀態緩存是應用中十分常見的需求,在須要處理的數據量較少時,使用手動狀態緩存就能夠解決大多數問題,但當狀況複雜時,還須要嘗試將緩存功能單獨拎出來解決,以便在業務開發過程當中更好地進行關注點分離
目前的實現都有各自的問題,但其探究過程十分有趣,最好的方式還是官方的支持,但目前還不能報太大指望