<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功能相關(guān)知識(shí)在這里(下)

          共 12431字,需瀏覽 25分鐘

           ·

          2022-04-19 10:40

          作者:lulu_up

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


          本篇承接上篇內(nèi)部, 所以是從第九點(diǎn)開始


          九、保留頁面scroll



          比如頁面上的table里有100條數(shù)據(jù), 我們想看第100條數(shù)據(jù), 那就要滾動(dòng)不少距離, 不少場(chǎng)景這種滾動(dòng)距離也是有必要保留的。


          這里使用的方法其實(shí)比較傳統(tǒng)啦, 首先在KeepAliveProvider 下發(fā)一個(gè)處理滾動(dòng)的方法:


          const handleScroll = useCallback(
                  (cacheId, event) => {
                      if (catheStates?.[cacheId]) {
                          const target = event.target
                          const scrolls = catheStates[cacheId].scrolls
                          scrolls[target] = target.scrollTop
                      }
                  },
                  [catheStates]
              )


          在Keeper組件里面接收并執(zhí)行:


          const { dispatch, mount, handleScroll } = useContext(CacheContext)

              useEffect(() => {
                  const onScroll = handleScroll.bind(null, cacheId)
                  (divRef?.current as any)?.addEventListener?.('scroll', onScroll, true)
                  return (divRef?.current as any)?.addEventListener?.('scroll', onScroll, true)
              }, [handleScroll])


          在Keeper里面將滾動(dòng)屬性賦予元素:


          useEffect(() => {
                  const catheState = catheStates[cacheId]
                  if (catheState && catheState.doms) {
                      const doms = catheState.doms
                      doms.forEach((dom: any) => {
                        (divRef?.current as any)?.appendChild?.(dom)
                      })

                  // 新增
                  doms.forEach((dom: any) => {
                      if (catheState.scrolls[dom]) {
                          dom.scrollTop = catheState.scrolls[dom]
                      }
                  })
                  } else {
                      mount({
                          cacheId,
                          reactElement: props.children
                      })
                  }
              }, [catheStates])


          這里如果不主動(dòng)增加賦予scroll的方法的話, 滾動(dòng)距離是不會(huì)被保存的, 因?yàn)镵eeper每次都是新的。


          十、KeepAliveProvider內(nèi)部 Keeper子組件內(nèi)部的CacheContext



          我們是把組件渲染在 KeepAliveProvider 里面, 那么如果某個(gè)Provider是在 KeepAliveProvider 內(nèi)部定義的, 則KeepAliveProvider級(jí)別的組件是無法使用 Consumer 拿到這個(gè)值的。


          這里就引出一個(gè)問題, 如何將 KeepAliveProvider 中的組件的上下文, 修改為Keeper組件的上下文。

          這里演示一下最直接的方式, 讓用戶傳入Provider與其value值。

          <Keeper 
             cacheId="home" 
             context={{ Provider: Provider, value: value }}>
              <Home />
           </Keeper>


          我們拿到這兩個(gè)值后直接在Keeper中修改reactElement的結(jié)構(gòu):


          mount({
                  cacheId,
                  reactElement: context ? 
                    <context.Provider 
                       value={context.value}>{props.children}</context.Provider> : 
                    props.children
              })


          當(dāng)檢測(cè)到context有值則直接在 props.children 外面套一層, 當(dāng)然這里存在一個(gè)多層Provider嵌套的問題沒有去解決, 因?yàn)橹饾u復(fù)雜起來它的實(shí)用性已經(jīng)在下降了, 接下來還有新的bug來襲。


          十一、需要傳值的組件



          大家有沒有發(fā)現(xiàn)上述組件所有邏輯, 都是直接寫在Keeper標(biāo)簽里面的, 并沒有任何的傳值, 但是比較常見的一種場(chǎng)景是下面這樣的:


          function Root (){
             const [n, setN] = useState(1)
           return 
             (
              <>
                 <button onClick={()=>setN(n+1)}>n+1</button>
                  <Keeper>
                    <Home n={n} />
                  </Keeper>
              </>
           )
          }


          這個(gè)n是Keeper外層傳遞給Home組件的, 這種寫法下會(huì)導(dǎo)致n雖然變化了但是Home里面不會(huì)響應(yīng)。


          這個(gè)bug我是這樣發(fā)現(xiàn)的, 當(dāng)我把這個(gè)插件用在我們團(tuán)隊(duì)的項(xiàng)目里的一個(gè)表格為主的頁面時(shí) , table一直顯示是空的, 并且輸入框也無法輸入值, 經(jīng)過測(cè)試發(fā)現(xiàn)其實(shí)值是有變化的, 只是沒有展示在組件的dom上。


          嘗試了好久后試了下react-activation 很遺憾它也有相同的問題, 那其實(shí)就說明這個(gè)bug很可能無法解決或者就是這個(gè)插件本身的架構(gòu)存在的問題。


          十二、為何這么奇怪的bug場(chǎng)景



          當(dāng)時(shí)這個(gè)bug折磨了我一天半的時(shí)間, 最后定位到外界的傳參已經(jīng)不能算是這個(gè)組件本身的參數(shù)了, 我們組件的實(shí)際渲染位置是 KeepAliveProvider 的第一層, 而Keeper的外層還在KeepAliveProvider的更內(nèi)層, 這就導(dǎo)致這些值的變化其實(shí)是沒有能夠影響到組件。


          可以理解為這些值的變化, 比如n的變化就如同window.n的改變一樣, react組件是不會(huì)去響應(yīng)這個(gè)變化的。


          那其實(shí)我們要做的就是讓外層傳入的值的變化, 可以帶動(dòng)組件的樣式變化 (逐漸入坑!)。


          十三、將props單獨(dú)拿出來



          我借鑒了網(wǎng)上另一種keep-alive組件的寫法, 把Keeper組件改為一個(gè)keeper的方法, 這個(gè)方法返回一個(gè)組件看, 這樣就可以接收一個(gè)props了, 也就把變量圈定在props這個(gè)范圍:


          const Home = keeper(HomePage, { cacheId: 'home' })


          function Root(){
           const [n, setN] = useState(1)
            return (
              <>
               <button onClick={()=>setN(n+1)}>n+1</button>
               <Home n={n}> // 此處可以傳值了
              </>
            )
          }


          這樣做的目的是讓開發(fā)者把能夠影響組件狀態(tài)的參數(shù)一口氣傳進(jìn)來, 比如之前一個(gè)Keeper里面可以有多個(gè)組件, 這種情況就不好控制哪些參數(shù)變化會(huì)導(dǎo)致哪些組件更新, 但以組件的方式可以明顯得知組件接收到的props里面的值的改變會(huì)導(dǎo)致組件更新。


          我想到的方案是, 在KeepAliveProvider里面新建propsObj, 用來專門儲(chǔ)存每個(gè)緩存組件的props, 之所以如此設(shè)計(jì)將其單獨(dú)拿出來, 是要把傳參與組件的邏輯拆分開, 不少邏輯會(huì)監(jiān)控catheStates的變化而執(zhí)行, 但是props的變化沒有必要觸發(fā)這些。


          const [propsObj, setPropsObj] = useState<any>();
              return (
              <CacheContext.Provider value={{ setPropsObj, propsObj }}>
                {props.children}
                
             //.... 略


          KeepAliveProvider 里面的渲染需要變一個(gè)形式, reactElement 變成組件了, 別忘了名字要變成大寫的。

              
          // 舊的
              // {reactElement}
              
              // 新的
              {propsObj && 
                <ReactElement {...propsObj[cacheId]}></ReactElement>}


          改裝一下Keeper文件, 首先要把文件名改為 keeper, 導(dǎo)出的方法要進(jìn)行一下更改。

           
          export default function (
             RealComponent: React.FunctionComponent<any>, { cacheId = '' }) {
             
             return function Keeper(props: any) {
           // ... 略


          Keeper內(nèi)mount方法的使用也稍作調(diào)整:

              
          mount({
                  cacheId,
                  ReactElement: RealComponent
              })


          關(guān)鍵的來了, 我們要在Keeper里面監(jiān)測(cè)props的變化, 來更新propsObj:


          const { propsObj, setPropsObj } = useContext(CacheContext)

              useEffect(() => {
                  setPropsObj({
                      ...propsObj,
                      [cacheId]: props
                  })
              }, [props])


          十四、緩存失敗的bug



          上述我們已經(jīng)把插件改裝了形式, 并且發(fā)現(xiàn)可以讓如下場(chǎng)景正常渲染, Home組件的props是外界傳入的:


          const Home = keeper(HomePage, { cacheId: 'home' })

          const RootComponent: React.FC = () => {
              return (
                  <KeepAliveProvider>
                      <Router>
                          <Routes>
                              <Route path={'/'} element={<Mid />} />
                          </Routes>
                      </Router>
                  </KeepAliveProvider>
              )
          }
          function Mid() {
              const [n, setN] = useState(1)
              return (
                  <div>
                      <button onClick={() => setN(n + 1)}>n+1</button>
                      <Home n={n}></Home>
                  </div>
              )
          }

          function HomePage(props: { n: number }) {
              return <div>home {props.n}</div>
          }


          但是此時(shí)如果切換頁面后再返回home頁面, home頁面的緩存是會(huì)失效的。


          其實(shí)是因?yàn)槲覀儗?shí)時(shí)監(jiān)控props的變化, 下次重新渲染時(shí)會(huì)導(dǎo)致props變化, 然后值就會(huì)被初始化了, 導(dǎo)致組件也恢復(fù)到了早期的配置, 可是.... 這不就是緩存失敗了嗎?


          每次組件props被重置就會(huì)導(dǎo)致組件的相關(guān)數(shù)據(jù)被重置, 嘗試把home組件做如下更改:


          function HomePage(props: { n: number }) {
              const [x, setX] = useState(1)
              return (
                  <div>
                      <button onClick={() => setX(x + 1)}>x + 1</button>
                      <div>home {props.n}</div>
                      <div>home: x {x}</div>
                  </div>
              )
          }


          上述寫法會(huì)導(dǎo)致每次激活home組件, 只能保留x的值, n的值會(huì)與傳入的相同。


          這種變化可能會(huì)導(dǎo)致bug, 假設(shè)只有 n > 2 才能讓 x > 3, 此時(shí)我們通過點(diǎn)擊事件讓 n = 5 , x = 4了, 此時(shí)切換到其他頁面再回來, 就變成了n = 1, x=4, 違背了我們的初始限制條件, 以此類推在真實(shí)復(fù)雜的開發(fā)環(huán)境中此現(xiàn)象會(huì)導(dǎo)致各種奇怪的問題。


          十五、認(rèn)知的代價(jià)



          上面的場(chǎng)景可以通過開發(fā)人員自己來控制, 理想情況是keep-alive插件只用來處理不需要外界傳參, 以及不會(huì)被外界參數(shù)的變化影響的組件, 但這就開始麻煩了。


          這類問題導(dǎo)致開發(fā)者在插件身上要花的學(xué)習(xí)成本提高, 使用成本提高, 并且如果某個(gè)組件本來不需要傳參, 我們用keep-alive包裹起來了, 后續(xù)又需要傳參了, 改變的成本想想都麻煩。


          網(wǎng)上現(xiàn)有(2022年04月10日17:16:22)組件的官網(wǎng)基本是沒有認(rèn)真的對(duì)用戶講述相關(guān)的問題, 往往都是以介紹"使用方法"與闡述自己的優(yōu)勢(shì)為主, 這就導(dǎo)致用戶被莫名其妙的bug折磨。


          傳遞 Provider 的方法也有問題, 需要傳遞可能不是本頁代碼的Provider, 難受的了啊。


          想要解決keep-alive相關(guān)問題的思路可以換一下, 最好是在react源碼里支持一波, 比如可以指定某些組件不被銷毀, 其實(shí)我們可以關(guān)注一下react18的后續(xù)版本, 現(xiàn)在這個(gè)時(shí)間段react18發(fā)布了正式版。


          十六、如何升級(jí)到react18



          方式一: create-react-app 創(chuàng)建新項(xiàng)目


          現(xiàn)階段直接使用下面的命令, 就可創(chuàng)建react18項(xiàng)目:


          npx create-react-app my_react



          下面這種使用 --template 指定模板的還不行, 因?yàn)槟0宕a還沒更新:


          npx create-react-app my_react --template typescript


          方式二: 老項(xiàng)目改裝


          首先直接把依賴?yán)锩娴膔eact 與 react-dom的版本號(hào)改成 "^18.0.0"即可。

          兩種方式都需要修改 index.js


          啟動(dòng)項(xiàng)目會(huì)有報(bào)錯(cuò)信息:



          舊版的index.js



          新版的index.js



          其他的沒有太多更改了。


          十七、react18 Offscreen 組件的用法



          Offscreen 允許 React 通過隱藏組件而不是卸載組件來保持這樣的狀態(tài), React 將調(diào)用與卸載時(shí)相同的生命周期鉤子, 但它也會(huì)保留 React 組件和 DOM 元素的狀態(tài)。


          React Activation 中也推薦大家關(guān)注這個(gè)屬性:



          Offscreen 是什么的官方說法可以看這篇文章里的翻譯: https://www.jianshu.com/p/184d981b8743



          Offscreen的測(cè)試用例:



          遺憾的是 Offscreen 組件并沒有在當(dāng)前版本推出, 其還處于不穩(wěn)定階段, 但我們可以通過 react18 里面的測(cè)試用例來預(yù)覽一下其用法:



          通過上述寫法還無法看出 Offscreen 到底如何使用, 只知道它可能是以組件的形式出現(xiàn), 并且需要傳入一個(gè)mode屬性, 更多用法期待官方盡快推出吧。


          讓我們一起期待 react18 來解決keep-alive這個(gè)問題吧, 這次就是這樣, 希望與你一起進(jìn)步。




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

          - END -


          瀏覽 146
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  国产一级做a爰片在线看免费 | 伊人高清无码在线视频s | 女人18片毛片90分钟免费 | a级黄片免费在线观看 | 秋霞丝鲁片一区二区三区手机在绒免 |