100行JavaScript代碼在React中優雅的實現簡單組件keep-Alive

  • 2019 年 10 月 11 日
  • 筆記

React是近些年出現比較優秀的前端框架,它的設計思想,源碼非常棒。

什麼是狀態保存?

假設有下述場景:

移動端中,用戶訪問了一個列表頁,上拉瀏覽列表頁的過程中,隨着滾動高度逐漸增加,數據也將採用觸底分頁加載的形式逐步增加,列表頁瀏覽到某個位置,用戶看到了感興趣的項目,點擊查看其詳情,進入詳情頁,從詳情頁退回列表頁時,需要停留在離開列表頁時的瀏覽位置上 類似的數據或場景還有已填寫但未提交的表單、管理系統中可切換和可關閉的功能標籤等,這類數據隨着用戶交互逐漸變化或增長,這裡理解為狀態,在交互過程中,因為某些原因需要臨時離開交互場景,則需要對狀態進行保存

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

如何實現 React 中的狀態保存

Vue 中,我們可以非常便捷地通過 <keep-alive> 標籤實現狀態的保存,該標籤會緩存不活動的組件實例,而不是銷毀它們 而在 React 中並沒有這個功能,曾經有人在官方提過功能 issues ,但官方認為這個功能容易造成內存泄露,表示暫時不考慮支持,所以我們需要自己想辦法了

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

手動保存狀態,是比較常見的解決方式,可以配合 React 組件的 componentWillUnmount 生命周期通過 redux 之類的狀態管理層對數據進行保存,通過 componentDidMount 周期進行數據恢復 在需要保存的狀態較少時,這種方式可以比較快地實現我們所需功能,但在數據量大或者情況多變時,手動保存狀態就會變成一件麻煩事了 作為程序員,當然是儘可能懶啦,為了不需要每次都關心如何對數據進行保存恢復,我們需要研究如何自動保存狀態

最初的版本react-keep-alive

1500行TypeScript代碼在React中實現組件keep-alive 我的這篇文章對源碼進行了解析,但是這個庫存在斷層現象,雖然可以緩存最後一次狀態渲染結果,但是後面數據變化無法再進行數據驅動。而且是藉助React.createPortal 藉助實現,我跟下面這個庫的作者都覺得這是多餘的,其實只需要抽取children屬性,再封裝一次HOC高階組件即可。

總體來說,react-keep-alive這個庫比較重,實現原理也不難,就是笨重,斷層,源碼跳來跳去,真的理清楚了就好

react-activation優雅的實現

效果實現:

庖丁解牛,源碼解析

最簡單版本的react中keep-alive實現演示地址

使用方式:開箱即用

import React, { useState } from 'react'  import { render } from 'react-dom'  import KeepAlive, { AliveScope } from './KeepAlive'    ...    function App() {    const [show, setShow] = useState(true)    return (      <div>        <button onClick={() => setShow(show => !show)}>Toggle</button>        <p>無 KeepAlive</p>        {show && <Counter />}        <p>有 KeepAlive</p>        {show && (          <KeepAlive id="Test">            <Counter />          </KeepAlive>        )}      </div>    )  }    ....      render(    <AliveScope>      <App />    </AliveScope>,    document.getElementById('root')  )

注意 : 緩存的虛擬DOM元素會儲存在AliveScope 組件中,所以它不能被卸載

使用AliveScope 配合KeepAlive即可達到緩存效果,類似react-keep-alive

首先我們看看AliveScope 組件做了什麼事情

export class AliveScope extends Component {    nodes = {}    state = {}      keep = (id, children) =>      new Promise(resolve =>        this.setState(          {            [id]: { id, children }          },          () => resolve(this.nodes[id])        )      )      render() {      return (        <Provider value={this.keep}>          {this.props.children}          {Object.values(this.state).map(({ id, children }) => (            <div              key={id}              ref={node => {                this.nodes[id] = node              }}            >              {children}            </div>          ))}        </Provider>      )    }  }

它的源碼只有幾十行,很簡單,這裡的this.props.children是虛擬DOM,經過Babel編譯和React處理,最終會轉化成真實DOM節點渲染

從零自己編寫一個mini-React框架 如果你不是很懂,那麼可以看我的這篇文章

逐步解析:

 {this.props.children}

是這個組件的所有子元素,必須要渲染

使用ReactContext API進行傳遞KEEP方法給所有的子孫組件,每次這個方法被調用,都會造成AliveScope 組件重新渲染,進而刷新子組件,並且返回一個真實的DOM節點,這個真實的DOM節點就可以被直接DOM操作。

這張思維導圖,可以很清楚的表示,我們的緩存實現方式,如果看不懂,慢慢往下看

KeepAlive組件的源碼

import React, { Component, createContext } from 'react'    const { Provider, Consumer } = createContext()    const withScope = WrappedCompoennt => props => (    <Consumer>{keep => <WrappedCompoennt {...props} keep={keep} />}</Consumer>  )        @withScope  class KeepAlive extends Component {    constructor(props) {      super(props)      this.init(props)    }      init = async ({ id, children, keep }) => {      const realContent = await keep(id, children)      this.placeholder.appendChild(realContent)    }      render() {      return (        <div          ref={node => {            this.placeholder = node          }}        />      )    }  }    export default KeepAlive

withScope是一個高階組件,將KeepAlive組件傳入,返回一個新的組件,這裡使用了裝飾器,@withScope.其實最終export default 的是withScope(KeepAlive)

這裡就是跟react-keep-alive的真正區別,withScope使用了context api捕獲了傳入的虛擬DOM節點,橋接了父組件以及KeepAlive組件的關聯,一旦children屬性改變,那麼withScope被刷新,進而傳入新的children屬性給KeepAlive組件,導致數據驅動可以進行組件刷新

這又印證了那句話

在計算機的世界裏,如果出現解決不了的問題,那就加一個中間層,如果還不行就加兩個 –來自不知名碼農Peter

這裡按照代碼運行邏輯,完整的解析了它的簡單緩存機制實現,思路整體比較清晰,加上代碼自己斷點調試難度應該比較低,個人覺得這個庫的設計和思想,都是不錯的,值得推廣,作者也是比較樂意解答問題。大家有問題可以在github上提問。

另外SegmentFault前端交流群還有名額,有需要的可以加我微信:CALASFxiaotan,裏面大量小姐姐哦

歡迎關注微信公眾號:前端巔峰

覺得不錯記得點個贊哦~ 以後會有更多的源碼解析