<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>

          關(guān)于 React 的 keep-alive 功能都在這里了(上)

          共 9753字,需瀏覽 20分鐘

           ·

          2022-05-13 08:59

          點擊上方?前端Q,關(guān)注公眾號

          回復(fù)加群,加入前端Q技術(shù)交流群

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

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

          背景

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

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

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

          一、插件調(diào)研

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

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

          image.png

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

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

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

          二、核心原理、

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

          image.png

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

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

          function?A(){
          ??return?(
          ????<div>
          ??????<B1>B1>

          ????div>
          ??)
          }

          再使用 appendChilddiv里面的dom元素全部轉(zhuǎn)移到"B組件"里面即可。

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

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

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

          image.png

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

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

          image.png

          四、插件的架構(gòu)設(shè)計介紹

          先看用法:

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

          ????????????????????????}
          ????????????????????/>
          ????????????????Routes>
          ????????????Router>
          ????????KeepAliveProvider>
          ??)

          我們使用 KeepAliveProvider 組件來儲存需要被緩存的組件的相關(guān)信息, 并且用來渲染被緩存的組件, 也就是充當(dāng)"A組件"的角色。

          KeepAliveProvider組件內(nèi)部使用 Keeper 組件來標(biāo)記組件應(yīng)該渲染在哪里? 也就是要用 Keeper"B1組件"+"B2組件"包裹起來, 這樣我們就知道初始化好的組件該放到哪里。

          cacheId也就是緩存的id, 每個id對應(yīng)一個組件的緩存信息, 后續(xù)會用來監(jiān)控每個緩存的組件是否被"激活", 以及清理組件緩存。

          五、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:?真正要渲染的內(nèi)容,
          ??status:?狀態(tài),
          ??doms?:?dom元素,
          ?}
          2. mount 用來初始化組件

          將組件狀態(tài)變?yōu)?'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 標(biāo)簽中的內(nèi)容
          5. div渲染需要緩存的組件

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

          6. 異步渲染的組件

          假設(shè)有如下這種異步的組件, 則無法獲取到正確的dom節(jié)點, 所以如果domchildNodes為空, 我們需要監(jiān)聽dom的狀態(tài), 當(dāng)dom內(nèi)被插入元素時執(zhí)行。

          ?function?HomePage()?{
          ????const?[show,?setShow]?=?useState(false)
          ????useEffect(()?=>?{
          ????????setShow(true)
          ????},?[])
          ????return?show???<div>homediv>:?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
          ????????????????????}
          ????????????????})
          ????????????}
          ????????}
          ????}
          }

          當(dāng)沒有獲取到 childNodes 則為div添加 "DOMNodeInserted"事件, 來監(jiān)測是否有dom插入到了div內(nèi)部。

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

          六、編寫渲染占位的Keeper

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

          "home">
          ??<Home?/>
          ??<User?/>
          ??<footer>footerfooter>
          </Keeper>

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

          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>
          }

          這里會多出一個div, 我也沒發(fā)現(xiàn)太好的辦法, 我嘗試使用doms把這個div元素替換掉, 這就會導(dǎo)致沒有react的數(shù)據(jù)驅(qū)動了, 也嘗試將這個dom 設(shè)置 "hidden = true" 然后將doms插入到這個div的兄弟節(jié)點, 但最后也沒成功。

          七、Portals屬性介紹

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

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

          ReactDOM.createPortal(child,?"目標(biāo)dom")

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

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

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

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

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

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

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

          image.png

          它的代碼如下:

          `????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

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

          ?useEffect(()?=>?{
          ????????const?cb?=?()?=>?{
          ????????????console.log('home被激活了')
          ????????}
          ????????cacheWatch(['home'],?cb)
          ????????return?()?=>?{
          ????????????removeCacheWatch(['home'],?cb)
          ????????}
          ????},?[])
          具體實現(xiàn)

          在KeepAliveProvider中定義activeCache方法:

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

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

          添加一個檢測方法:

          每次都把callback放到對應(yīng)的對象身上。

          ?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ù)進(jìn)行刪除。

          ?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
          ????}?

          end

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

          關(guān)于本文

          作者:lulu_up


          往期推薦


          邊緣渲染是如何提高前端性能的?
          對話Svelte未來,Rust 編譯器?構(gòu)建大型應(yīng)用?
          收藏!史上最全 Vue 前端代碼風(fēng)格指南

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...

          點個在看支持我吧
          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  caopen在线视频 | 日韩在线播放视频 | 日本欧美一区 | 欧美内射久久 | 色综合网站 |