<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一些關于react的keep-alive功能相關知識在這里(上)

          共 22312字,需瀏覽 45分鐘

           ·

          2022-04-13 09:50

          作者:lulu_up

          來源:SegmentFault  思否社區(qū) 


          下一篇講這類插件的"大坑", 如果你想全面了解的話一定要讀下一篇哦。


          背景



          這是在2022年開發(fā)中PM提的一個需求, 某個table被用戶輸入了一些搜搜條件并且瀏覽到了第3頁, 那如果我跳轉到其他路由后返回當前頁面, 希望搜索條件還在, 并且仍處于第三頁, 這不就是vue里面的keep-alive標簽嗎, 但我當前的項目是用react編寫的。


          此次講述了我經歷了 "使用外部插件"-> "放棄外部插件"-> "學習并自研插件"-> "理解了相關插件的困境" -> "期待react18的Offscreen", 所以結論是推薦耐心等待react18的自支持, 但是學習當前類似插件的原理對自己還是很有啟發(fā)的。


          一個庫不能只說自己的優(yōu)點也要把缺點展示出來, 否則會給使用者代碼隱患, 但我閱讀了很多網(wǎng)上的文章與官網(wǎng), 大多都沒有講出相關的原理的細節(jié), 并且沒有人對當前存在的bug進行分析, 我這里會對相關奇怪的問題進行詳細的講解, 我下面展示代碼是參考了網(wǎng)上的幾種方案后稍作改良的。


          一、插件調研



          我們一起看一下市場上現(xiàn)在有哪些'成熟'的方案。


          第一個: react-keep-alive : 官網(wǎng)很正規(guī), 851 Star, 用法上也與vue的keep-alive很接近, 但是差評太多了, 以及3年沒更新了, 并且很多網(wǎng)上的文章也都說這個庫很坑, 一起看看它的評論吧 (抬走下一位)。


          ???? 

          第二個: react-router-cache-route : 這個庫是針對路由級別的一個緩存, 無法對組件級別生效, 引入后要替換掉當前的路由組件庫, 風險不小并且緩存的量級太大了 (抬走下一位)。


          第三個: react-activation : 這個庫是網(wǎng)上大家比較認可的庫, issues也比較少并且不'致命', 并且可以支持組件級別的緩存( 其實它做不到, 還是有bug ), 我嘗試著使用到自己團隊的項目里后效果還可以, 但是由于此插件沒有大團隊支持并且內部全是中文, 最后也沒有進行使用。


          通過上述調研, 讓我對 react-activation 的原理產生了興趣, 遂想在團隊內部開發(fā)一款類似的插件不就可以了嗎, 對keep-alive的探究從此揭開序幕。


          二、核心原理



          先贅述一下前提, react的虛擬dom結構是一棵樹, 這棵樹的某個節(jié)點被移除會導致所有子節(jié)點也被銷毀 所以寫代碼時才需要用 Memo進行包裹。(記住這張圖)



          比如我想緩存"B2組件"的狀態(tài), 那其實要做的就是讓"B組件"被銷毀時 "B2組件不被銷毀", 從圖上可知當"B組件"被銷毀時"A組件"是不會被銷毀的, 因為"A組件"不在"B組件"的下級, 所以我們要做的就是讓"A組件"來生成"B2組件", 再把"B2"組件插入到"B組件內部"。


          所謂的在"A組件"下渲染, 就是在"A組件"里面:


          function A(){
            return (
              <div>
                <B1></B1>
              </div>
            )
          }


          再使用 appendChild 將div里面的dom元素全部轉移到"B組件"里面即可。


          三、appendChild后react依然正常執(zhí)行



          雖然使用appendChild把"A組件"里面的dom元素插入到"B組件", 但是react內部的各種渲染已經完成, 比如我們在 "B1組件" 內使用 useState 定義了一個變量叫 'n' , 當 'n' 變化時觸發(fā)的dom變化也都已經被react記錄, 所以不會影響每次進行dom diff 后的元素操作。


          并且在"A組件"下面也可以使用 "Consumer" 接收到"A組件"外部的 "Provider", 但也引出一個問題, 就是如果不是"A組件"外的"Provider"無法被接收到, 下面是react-actication的處理方式:


          ???? 

          其實這樣侵入react源代碼邏輯的操作還是要慎重, 我們也可以用粗俗一點的方式稍微代替一下, 主要利用 Provider 可以重復寫的特性, 將Provider與其value傳入進去實現(xiàn)context的正常, 但是這樣也顯然是不友好的。


          所以 react-activation 官網(wǎng)才會注明下面這段話:



          四、插件的架構設計介紹



          我們使用KeepAliveProvider組件來儲存需要被緩存的組件的相關信息,并且用來儲存需要被緩存的組件的相關信息,并且用來渲染被緩存的組件,也就是充當“A組件”的角色。

          KeepAliveProvider組件內部使用Keeper組件來標記組件應該渲染在哪里?也就是要用Keeper將“B1組件”+“B2組件”包裹起來,這樣我們就知道初始化好的組件應該放到哪里。cacheld也就是緩存的id,每個id對應一個組件的緩存信息,后續(xù)會用來監(jiān)控每個緩存的組件是否被激活,以及清理組件的緩存。


          看用法:


          const RootComponent: React.FC = () => (
                  <KeepAliveProvider>
                      <Router>
                          <Routes>
                              <Route path={'/'} element={
                                    <Keeper cacheId={'home'}> <Home/> </Keeper>
                                  }
                              />
                          </Routes>
                      </Router>
                  </KeepAliveProvider>
            )


          五、KeepAliveProvider開發(fā)



          這里先列出一個"概念代碼", 因為直接看完整的代碼會暈掉。


          import CacheContext from './cacheContext'
          const KeepAliveProvider: React.FC = (props) => {
           const [catheStates, dispatch]: any = useReducer(cacheReducer, {})
               const mount = useCallback(
                  ({ cacheId, reactElement }) => {
                      if (!catheStates || !catheStates[cacheId]) {
                          dispatch({
                              type: cacheTypes.CREATE,
                              payload: {
                                  cacheId,
                                  reactElement
                              }
                          })
                      }
                  },
                  [catheStates]
              )
           return (
                  <CacheContext.Provider value={{ catheStates, mount }}>
                      {props.children}
                      {Object.values(catheStates).map((item: any) => {
                          const { cacheId = '', reactElement } = item
                          const cacheState = catheStates[`${cacheId}`];
                          const handleDivDom = (divDom: Element) => {
                               const doms = Array.from(divDom.childNodes)
                                  if (doms?.length) {
                                      dispatch({
                                          type: cacheTypes.CREATED,
                                          payload: {
                                              cacheId,
                                              doms
                                          }
                                      })
                                  }
                          }
                          return (
                              <div 
                               key={`${cacheId}`} 
                               id={`cache-外層渲染-${cacheId}`} 
                               ref={(divDom) => divDom && handleDivDom(divDom)}>
                                  {reactElement}
                              </div>
                  </CacheContext.Provider>
              )
          }

          export default KeepAliveProvider


          代碼講解


          1. catheStates 存儲所有的緩存信息

          它的數(shù)據(jù)格式如下:


          {
            cacheId: 緩存id,
            reactElement: 真正要渲染的內容,
            status: 狀態(tài),
            doms?: dom元素,
           }


          2. mount 用來初始化組件

          將組件狀態(tài)變?yōu)?nbsp;'CREATE', 并且將要渲染的組件儲存起來, 就是上圖里面"B1組件",

          const mount = useCallback(({ cacheId, reactElement }) => {
                      if (!catheStates || !catheStates[cacheId]) {
                          dispatch({
                              type: cacheTypes.CREATE,
                              payload: {
                                  cacheId,
                                  reactElement}
                          })
                      }
                  },
                  [catheStates]
              )

          3. CacheContext 傳遞與儲存信息

          CacheContext 是我們專門創(chuàng)建用來儲存數(shù)據(jù)的, 他會向各個 Keeper 分發(fā)各種方法。


          import React from "react";
          let CacheContext = React.createContext()
          export default CacheContext;


          4. {props.children} 渲染 KeepAliveProvider 標簽中的內容

          5. div渲染需要緩存的組件

          這里放一個div作為渲染組件的容器, 當我們可以獲取到這個div的實例時則對其childNodes儲存到catheStates, 但是這里有個問題, 這種寫法只能處理同步渲染的子組件, 如果組件異步渲染則無法儲存正確的childNodes。

          6. 異步渲染的組件

          假設有如下這種異步的組件, 則無法獲取到正確的dom節(jié)點, 所以如果dom的childNodes為空, 我們需要監(jiān)聽dom的狀態(tài), 當dom內被插入元素時執(zhí)行。
           
          function HomePage() {
              const [show, setShow] = useState(false)
              useEffect(() => {
                  setShow(true)
              }, [])
              return show ? <div>home</div>: null;
           }


          將handleDivDom方法的代碼做一些修改:


          let initDom = false
          const handleDivDom = (divDom: Element) => {
              handleDOMCreated()
              !initDom && divDom.addEventListener('DOMNodeInserted', handleDOMCreated)
              function handleDOMCreated() {
                  if (!cacheState?.doms) {
                      const doms = Array.from(divDom.childNodes)
                      if (doms?.length) {
                          initDom = true
                          dispatch({
                              type: cacheTypes.CREATED,
                              payload: {
                                  cacheId,
                                  doms
                              }
                          })
                      }
                  }
              }
          }


          當沒有獲取到 childNodes 則為div添加 "DOMNodeInserted"事件,

          來監(jiān)測是否有dom插入到了div內部。


          所以總結來說, 上述代碼就是負責了初始化相關數(shù)據(jù), 并且負責渲染組件, 但是具體渲染什么組件還需要我們使用Keeper組件。


          六、編寫渲染占位的Keeper


          在使用插件的時候, 我們實際需要被緩存的組件都是寫在Keeper組件里的, 就像下面這種寫法:


          <Keeper cacheId="home">
            <Home />
            <User />
            <footer>footer</footer>
          </Keeper>


          import React, { useContext, useEffect } from 'react'
          import CacheContext from './cacheContext'

          export default function Keeper(props: any) {
              const { cacheId } = props
              const divRef = React.useRef(null)
              const { catheStates, dispatch, mount } = useContext(CacheContext)
              useEffect(() => {
                  const catheState = catheStates[cacheId]
                  if (catheState && catheState.doms) {
                      const doms = catheState.doms
                      doms.forEach((dom: any) => {
                          (divRef?.current as any)?.appendChild?.dom
                      })
                  } else {
                      mount({
                          cacheId,
                          reactElement: props.children
                      })
                  }
              }, [catheStates])
              return <div id={`keeper-原始位置-${cacheId}`} ref={divRef}></div>
          }


          七、Portals屬性介紹


          看到網(wǎng)上有些插件沒有使用 appendChild 而是使用react提供的 來實現(xiàn)的, 感覺挺好玩的就在這里也聊一下。


          此時我們并不要真的在Keeper組件里面來渲染組件,把props.children儲存起來,在Keeper里面放一個div來占位,并且當檢測到有數(shù)據(jù)中有需要被緩存的dom時,則使用appendChild把dom放到自己的內部。


          Portal 提供了一種將子節(jié)點渲染到存在于父組件以外的 DOM 節(jié)點的優(yōu)秀的方案, 直白說就是可以指定我要把 child 渲染到哪個dom元素中, 用法如下:


          ReactDOM.createPortal(child, "目標dom")


          react官網(wǎng)是這樣描述的: 一個 portal 的典型用例是當父組件有 overflow: hidden 或 z-index 樣式時,但你需要子組件能夠在視覺上“跳出”其容器。例如,對話框、懸浮卡以及提示框:


          由于這里需要指定在哪里渲染 child, 所以大需要有明確的child屬性與目標dom, 但是我們這個插件可能更適合異步操作, 也就是我們只是將數(shù)據(jù)放在 catheStates 里面, 需要取的時候來取, 而不是渲染時就要明確指定的形式來設計。


          八、監(jiān)控緩存被激活


          我們要實時監(jiān)控到底哪個組件被"激活", "激活"的定義是組件被初始化后被緩存起來, 之后的每次使用緩存都叫"激活", 并且每次組件被激活調用 activeCache 方法來告訴用戶當前哪個組件被"激活"了。


          為什么要告訴用戶哪個組件被激活了? 大家可以想想這樣一個場景, 用戶點擊了table的第三條數(shù)據(jù)的編輯按鈕跳轉到編輯頁面, 編輯后返回列表頁, 此時可能需要我們更新一下列表里第三條的狀態(tài), 此時就需要知道哪些組件被激活了。


          還有一種情況如下圖所示, 這是一種鼠標懸停會出現(xiàn)tip提示語, 如果此時點擊按鈕發(fā)生跳轉頁面會導致, 當你返回列表頁面時這個tip竟然還在....


          當然我指的不是element-ui, 是我們自己的ui庫, 當時看了一下原因, 是因為這個組件只有檢測到鼠標離開某些元素才會讓tip消失, 但是跳頁了并且當前頁面的所有dom被 keep-alive被緩存下來了, 導致了這個tip沒有被清理。



          它的代碼如下:


          useEffect(() => {
                  const catheState = catheStates[cacheId]
                  if (catheState && catheState.doms) {
                      console.log('激活了:', cacheId)
                      activeCache(cacheId)
                  }
              }, [])


          之所以useEffect的參數(shù)只傳了個空數(shù)組, 因為每次組件被"激活"都可以執(zhí)行, 因為每次Keeper組件每次會被銷毀的, 所以這里可以執(zhí)行。


          最終使用演示

          在組件中使用來檢測指定的組件是否被更新, 第一個參數(shù)是要監(jiān)測的id, 也就是Keeper身上的cacheId, 第二個參數(shù)是callback。

          用戶使用插件時, 可以在自己的組件內按下面的寫法來進行監(jiān)控:

          useEffect(() => {
                  const cb = () => {
                      console.log('home被激活了')
                  }
                  cacheWatch(['home'], cb)
                  return () => {
                      removeCacheWatch(['home'], cb)
                  }
              }, [])

          具體實現(xiàn)

          在KeepAliveProvider中定義activeCache方法:

          每次激活組件, 就去數(shù)組內尋找監(jiān)聽方法進行執(zhí)行。


          const [activeCacheObj, setActiveCacheObj] = useState<any>({})
              const activeCache = useCallback(
                  (cacheId) => {
                      if (activeCacheObj[cacheId]) {
                          activeCacheObj[cacheId].forEach((fn: any) => {
                              fn(cacheId)
                          })
                      }
                  },
                  [catheStates, activeCacheObj]
              )


          添加一個檢測方法:


          每次都把callback放到對應的對象身上。


          const cacheWatch = useCallback(
               (ids: string[], fn) => {
                  ids.forEach((id: string) => {
                      if (activeCacheObj[id]) {
                          activeCacheObj[id].push(fn)
                      } else {
                          activeCacheObj[id] = [fn]
                      }
                  })
                  setActiveCacheObj({
                      ...activeCacheObj
                  })
                },
               [activeCacheObj]
              )


          還要有一個移除監(jiān)控的方法:

              

          const removeCacheWatch = (ids: string[], fn: any) => {
                  ids.forEach((id: string) => {
                      if (activeCacheObj[id]) {
                          const index = activeCacheObj[id].indexOf(fn)
                          activeCacheObj.splice(index, 1)
                      }
                  })
                  setActiveCacheObj({
                      ...activeCacheObj
                  })
              }

               刪除緩存的方法, 需要在 cacheReducer 里面增加刪除方法, 注意這里需要每個remove所有dom, 而不是僅對 cacheStates 的數(shù)據(jù)進行刪除。

          case cacheTypes.DESTROY:
              if (cacheStates[payload.cacheId]) {
                  const doms = cacheStates?.[payload.cacheId]?.doms
                  if (doms) {
                      doms.forEach((element) => {
                          element.remove()
                      })
                  }
              }
              delete cacheStates[payload.cacheId]
              return {
                  ...cacheStates
              }


          下一篇講這類插件的"大坑", 如果你想全面了解的話一定要讀下一篇哦, 這次就是這樣, 希望與你一起進步。




          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復“ 入群 ”即可加入我們的技術交流群,收獲更多的技術文章~

          - END -


          瀏覽 57
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  波多野结衣AV网站 | 国产黄色影院 | 亚洲艾薇在线观看 | 国产精品美女久久久久AV夜色 | 一级黄色免费观看 |