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


圖文無關javascript

什麼是狀態保存?

假設有下述場景:vue

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

相似的數據或場景還有已填寫但未提交的表單管理系統中可切換和可關閉的功能標籤等,這類數據隨着用戶交互逐漸變化或增加,這裏理解爲狀態,在交互過程當中,由於某些緣由須要臨時離開交互場景,則須要對狀態進行保存react

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

如何實現 React 中的狀態保存

Vue 中,咱們能夠很是便捷地經過 <keep-alive> 標籤實現狀態的保存,該標籤會緩存不活動的組件實例,而不是銷燬它們程序員

而在 React 中並無這個功能,曾經有人在官方提過功能 issues ,但官方認爲這個功能容易形成內存泄露,表示暫時不考慮支持,因此咱們須要本身想辦法了github

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

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

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

做爲程序員,固然是儘量懶啦,爲了避免須要每次都關心如何對數據進行保存恢復,咱們須要研究如何自動保存狀態緩存

經過路由實現自動狀態保存(一般使用 react-router

既然 React 中狀態的丟失是因爲路由切換時卸載了組件引發的,那能夠嘗試從路由機制上去入手,改變路由對組件的渲染行爲

咱們有如下的方式去實現這個功能

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

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

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

    重寫路由庫成本是通常開發者沒法承受的,且徹底替換掉路由方案是一個風險較大的事情,須要較爲慎重地考慮

  3. 基於 <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>

    上述是最簡的調整方式,實際狀況中也須要考慮隱藏狀態下 matchnull 致使組件報錯的問題,且因爲再也不是組件卸載,因此和 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 中已修復)
  • React 合成事件冒泡失效
  • 其餘未發現的功能

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

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

結語

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

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

相關文章
相關標籤/搜索