「React進階」只用兩個自定義 Hooks 就能替代 React-Redux ?

前言

以前有朋友問我,React Hooks 可否解決 React 項目狀態管理的問題。這個問題讓我思索了好久,最後得出的結論是:能,不過須要兩個自定義 hooks 去實現。那麼具體如何實現的呢? 那就是今天要講的內容了。前端

經過本文,你可以學習如下內容:react

  • useContext ,useRef ,useMemo,useEffect 的基本用法。
  • 如何將不一樣組件的自定義 hooks 創建通訊,共享狀態。
  • 合理編寫自定義 hooks , 分析 hooks 之間的依賴關係。
  • 自定義 hooks 編寫過程當中一些細節問題。

帶着如上的知識點,開啓閱讀之旅吧~(創做不易,但願屏幕前的你能給筆者賞個贊,以此鼓勵我繼續創做前端硬文。)面試

一 設計思路

首先,看一下要實現的兩個自定義 hooks 具體功能。redux

  • useCreateStore 用於產生一個狀態 Store ,經過 context 上下文傳遞 ,爲了讓每個自定義 hooks useConnect 都能獲取 context 裏面的狀態屬性。
  • useConnect 使用這個自定義 hooks 的組件,能夠獲取改變狀態的 dispatch 方法,還能夠訂閱 state ,被訂閱的 state 發生變化,組件更新。

如何讓不一樣組件的自定義 hooks 共享狀態並實現通訊呢?api

首先不一樣組件的自定義 hooks ,能夠經過 useContext 得到共有狀態,並且還須要實現狀態管理和組件通訊,那麼就須要一個狀態調度中心來統一作這些事,能夠稱之爲 ReduxHooksStore ,它具體作的事情以下:數組

  • 全局管理 state, state 變化,通知對應組件更新。
  • 收集使用 useConnect 組件的信息。組件銷燬還要清除這些信息。
  • 維護並傳遞負責更新的 dispatch 方法。
  • 一些重要 api 要暴露給 context 上下文,傳遞給每個 useConnect

1 useCreateStore 設計

首先 useCreateStore 是在靠近根部組件的位置的, 並且全局只須要一個,目的就是建立一個 Store ,並經過 Provider 傳遞下去。緩存

使用:markdown

const store = useCreateStore( reducer , initState )
複製代碼

參數:dom

  • reducer :全局 reducer,純函數,傳入 state 和 action ,返回新的 state 。
  • initState : 初始化 state 。

返回值:爲 store 暴露的主要功能函數。異步

2 Store設計

Store 爲上述所說的調度中心,接收全局 reducer ,內部維護狀態 state ,負責通知更新 ,收集用 useConnect 的組件。

const Store = new ReduxHooksStore(reducer,initState).exportStore()
複製代碼

參數:接收兩個參數,透傳 useCreateStore 的參數。

3 useConnect設計

使用 useConnect 的組件,將得到 dispatch 函數,用於更新 state ,還能夠經過第一個參數訂閱 state ,被訂閱的 state 改變 ,會讓組件更新。

// 訂閱 state 中的 number 
const mapStoreToState = (state)=>({ number: state.number  })
const [ state , dispatch ] = useConnect(mapStoreToState)
複製代碼

參數:

  • mapStoreToState:將 Store 中 state ,映射到組件的 state 中,能夠作視圖渲染使用。
  • 若是沒有第一個參數,那麼只提供 dispatch 函數,不會訂閱 state 變化帶來的更新。

返回值:返回值是一個數組。

  • 數組第一項:爲映射的 state 的值。
  • 數組第二項:爲改變 state 的 dispatch 函數。

4 原理圖

7.jpg

二 useCreateStore

export const ReduxContext = React.createContext(null)
/* 用於產生 reduxHooks 的 store */
export function useCreateStore(reducer,initState){
   const store = React.useRef(null)
   /* 若是存在——不須要從新實例化 Store */
   if(!store.current){
       store.current  = new ReduxHooksStore(reducer,initState).exportStore()
   }
   return store.current
}
複製代碼

useCreateStore 主要作的是:

  • 接收 reducerinitState ,經過 ReduxHooksStore 產生一個 store ,不指望把 store 所有暴露給使用者,只須要暴露核心的方法,因此調用實例下的 exportStore抽離出核心方法。

  • 使用一個 useRef 保存核心方法,傳遞給 Provider

三 狀態管理者 —— ReduxHooksStore

接下來看一下核心狀態 ReduxHooksStore 。

import { unstable_batchedUpdates } from 'react-dom'
class ReduxHooksStore {
    constructor(reducer,initState){
       this.name = '__ReduxHooksStore__'
       this.id = 0
       this.reducer = reducer
       this.state = initState
       this.mapConnects = {}
    }
    /* 須要對外傳遞的接口 */
    exportStore=()=>{
        return {
            dispatch:this.dispatch.bind(this),
            subscribe:this.subscribe.bind(this),
            unSubscribe:this.unSubscribe.bind(this),
            getInitState:this.getInitState.bind(this)
        }
    }
    /* 獲取初始化 state */
    getInitState=(mapStoreToState)=>{
        return mapStoreToState(this.state)
    }
    /* 更新須要更新的組件 */
    publicRender=()=>{
        unstable_batchedUpdates(()=>{ /* 批量更新 */
            Object.keys(this.mapConnects).forEach(name=>{
                const { update } = this.mapConnects[name]
                update(this.state)
            })
        })
    }
    /* 更新 state */
    dispatch=(action)=>{
       this.state = this.reducer(this.state,action)
       // 批量更新
       this.publicRender()
    }
    /* 註冊每一個 connect */
    subscribe=(connectCurrent)=>{
        const connectName = this.name + (++this.id)
        this.mapConnects[connectName] =  connectCurrent
        return connectName
    }
    /* 解除綁定 */
    unSubscribe=(connectName)=>{
        delete this.mapConnects[connectName]
    }
}
複製代碼

狀態

  • reducer:這個 reducer 爲全局的 reducer ,由 useCreateStore 傳入。
  • state:全局保存的狀態 state ,每次執行 reducer 會獲得新的 state 。
  • mapConnects:裏面保存每個 useConnect 組件的更新函數。用於派發 state 改變帶來的更新。

方法

負責初始化:

  • getInitState:這個方法給自定義 hooks 的 useConnect 使用,用於獲取初始化的 state 。
  • exportStore:這個方法用於把 ReduxHooksStore 提供的核心方法傳遞給每個 useConnect 。

負責綁定|解綁:

  • subscribe: 綁定每個自定義 hooks useConnect 。
  • unSubscribe:解除綁定每個 hooks 。

負責更新:

  • dispatch:這個方法提供給業務組件層,每個使用 useConnect 的組件能夠經過 dispatch 方法改變 state ,內部原理是經過調用 reducer 產生一個新的 state 。

  • publicRender:當 state 改變須要通知每個使用 useConnect 的組件,這個方法就是通知更新,至於組件需不須要更新,那是 useConnect 內部須要處理的事情,這裏還有一個細節,就是考慮到 dispatch 的觸發場景能夠是異步狀態下,因此用 React-DOM 中 unstable_batchedUpdates 開啓批量更新原則。

四 useConnect

useConnect 是整個功能的核心部分,它要作的事情是獲取最新的 state ,而後經過訂閱函數 mapStoreToState 獲得訂閱的 state ,判斷訂閱的 state 是否發生變化。若是發生變化渲染最新的 state 。

export function useConnect(mapStoreToState=()=>{}){
    /* 獲取 Store 內部的重要函數 */
   const contextValue = React.useContext(ReduxContext)
   const { getInitState , subscribe ,unSubscribe , dispatch } = contextValue
   /* 用於傳遞給業務組件的 state */
   const stateValue = React.useRef(getInitState(mapStoreToState))

   /* 渲染函數 */
   const [ , forceUpdate ] = React.useState()
   /* 產生 */
   const connectValue = React.useMemo(()=>{
       const state =  {
           /* 用於比較一次 dispatch 中,新的 state 和 以前的state 是否發生變化 */
           cacheState: stateValue.current,
           /* 更新函數 */
           update:function (newState) {
               /* 獲取訂閱的 state */
               const selectState = mapStoreToState(newState)
               /* 淺比較 state 是否發生變化,若是發生變化, */
               const isEqual = shallowEqual(state.cacheState,selectState)
               state.cacheState = selectState
               stateValue.current  = selectState
               if(!isEqual){
                   /* 更新 */
                   forceUpdate({})
               }
           }
       }
       return state
   },[ contextValue ]) // 將 contextValue 做爲依賴項。

   React.useEffect(()=>{
       /* 組件掛載——註冊 connect */
       const name =  subscribe(connectValue)
       return function (){
            /* 組件卸載 —— 解綁 connect */
           unSubscribe(name)
       }
   },[ connectValue ]) /* 將 connectValue 做爲 useEffect 的依賴項 */

   return [ stateValue.current , dispatch ]
}
複製代碼

初始化

  • 用 useContext 獲取上下文中, ReduxHooksStore 提供的核心函數。
  • 用 useRef 來保存獲得的最新的 state 。
  • 用 useState 產生一個更新函數 forceUpdate ,這個函數只是更新組件。

註冊|解綁流程

  • 註冊: 經過 useEffect 來向 ReduxHooksStore 中註冊當前 useConnect 產生的 connectValue ,connectValue 是什麼立刻會講到。subscribe 用於註冊,會返回當前 connectValue 的惟一標識 name 。

  • 解綁:在 useEffect 的銷燬函數中,能夠用調用 unSubscribe 傳入 name 來解綁當前的 connectValue

connectValue是否更新組件

  • connectValue :真正地向 ReduxHooksStore 註冊的狀態,首先用 useMemo 來對 connectValue 作緩存,connectValue 爲一個對象,裏面的 cacheState 保留了上一次的 mapStoreToState 產生的 state ,還有一個負責更新的 update 函數。

  • 更新流程 : 當觸發 dispatch 在 ReduxHooksStore 中,會讓每個 connectValue 的 update 都執行, update 會觸發映射函數 mapStoreToState 來獲得當前組件想要的 state 內容。而後經過 shallowEqual 淺比較新老 state 是否發生變化,若是發生變化,那麼更新組件。完成整個流程。

  • shallowEqual : 這個淺比較就是 React 裏面的淺比較,在第 11 章已經講了其流程,這裏就不講了。

分清依賴關係

  • 首先自定義 hooks useConnect 的依賴關係是上下文 contextValue 改變,那麼說明 store 發生變化,因此從新經過 useMemo 產生新的 connectValue 。因此 useMemo 依賴 contextValue。

  • connectValue 改變,那麼須要解除原來的綁定關係,從新綁定。useEffect 依賴 connectValue。

侷限性

整個 useConnect 有一些侷限性,好比:

  • 沒有考慮 mapStoreToState 可變性,沒法動態傳入 mapStoreToState 。
  • 淺比較,不能深層次比較引用數據類型。

五 使用與驗證效果

接下來就是驗證效果環節,我模擬了組件通訊的場景。

根部組件注入 Store

import { ReduxContext , useConnect , useCreateStore } from './hooks/useRedux'
function Index(){
    const [ isShow , setShow ] =  React.useState(true)
    console.log('index 渲染')
    return <div> <CompA /> <CompB /> <CompC /> {isShow && <CompD />} <button onClick={() => setShow(!isShow)} >點擊</button> </div>
}

function Root(){
    const store = useCreateStore(function(state,action){
        const { type , payload } =action
        if(type === 'setA' ){
            return {
                ...state,
                mesA:payload
            }
        }else if(type === 'setB'){
            return {
                ...state,
                mesB:payload
            }
        }else if(type === 'clear'){ //清空
            return  { mesA:'',mesB:'' }
        }
        else{
            return state
        }
    },
    { mesA:'111',mesB:'111' })
    return <div> <ReduxContext.Provider value={store} > <Index/> </ReduxContext.Provider> </div>
}
複製代碼

Root根組件

  • 經過 useCreateStore 建立一個 store ,傳入 reducer 和 初始化的值 { mesA:'111',mesB:'111' }
  • 用 Provider 傳遞 store。

Index組件

  • 有四個子組件 CompA , CompB ,CompC ,CompD 。其中 CompD 是 動態掛載的。

業務組件使用

function CompA(){
    const [ value ,setValue ] = useState('')
    const [state ,dispatch ] = useConnect((state)=> ({ mesB : state.mesB }) )
    return <div className="component_box" > <p> 組件A</p> <p>組件B對我說 : {state.mesB} </p> <input onChange={(e)=>setValue(e.target.value)} placeholder="對B組件說" /> <button onClick={()=> dispatch({ type:'setA' ,payload:value })} >肯定</button> </div>
}

function CompB(){
    const [ value ,setValue ] = useState('')
    const [state ,dispatch ] = useConnect((state)=> ({ mesA : state.mesA }) )
    return <div className="component_box" > <p> 組件B</p> <p>組件A對我說 : {state.mesA} </p> <input onChange={(e)=>setValue(e.target.value)} placeholder="對A組件說" /> <button onClick={()=> dispatch({ type:'setB' ,payload:value })} >肯定</button> </div>
}

function CompC(){
    const [state  ] = useConnect((state)=> ({ mes1 : state.mesA,mes2 : state.mesB }) )
    return <div className="component_box" > <p>組件A : {state.mes1} </p> <p>組件B : {state.mes2} </p> </div>
}

function CompD(){
    const [ ,dispatch  ] = useConnect( )
    console.log('D 組件更新')
    return <div className="component_box" > <button onClick={()=> dispatch({ type:'clear' })} > 清空 </button> </div>
}

複製代碼
  • CompA 和 CompB 模擬組件雙向通訊。
  • CompC 組件接收 CompA 和 CompB 通訊內容,並映射到 mes1 ,mes2 屬性上。
  • CompD 沒有 mapStoreToState ,沒有訂閱 state ,state 變化組件不會更新,只是用 dispatch 清空狀態。

效果

8.gif

六 總結

本文經過兩個自定義 hooks 實現了 React-Redux 的基本功能,這個模式在真實項目中可使用嗎? 我以爲若是是小型項目,是徹底可使用的,對於大型項目仍是用 React Redux 或者其餘成熟的狀態管理工具。

《React進階實踐指南》小冊已經上線

今天給你們推薦一本掘金小冊 《React進階實踐指南》,本文中的自定義 hooks 也是小冊自定義 hooks 章節中的一個案例。小冊還有不少自定義 hooks 設計案例,並且自定義 hooks 設計和實踐章節都在持續更新維護中,匯聚了筆者多年的心血,感興趣的同窗能夠了解如下,下面是小冊的介紹。

本小冊從基礎進階篇,優化進階篇,原理進階篇,生態進階篇,實踐進階篇,五個方向詳細探討 React 使用指南 和 原理介紹。

1.jpg

  • 在基礎進階篇裏,將從新認識react中 state,props,ref,context等模塊,詳解其基本使用和高階玩法。
  • 在優化進階篇裏,將講解 React性能調優和細節處理,讓React寫的更優雅。
  • 在原理進階篇裏,將針對React幾個核心模塊原理進行闡述,一次性搞定面試遇到React原理問題。
  • 在生態進階篇裏,將重溫React重點生態的用法,從原理角度分析內部運行的機制。
  • 在實踐進階篇裏,將串聯前幾個模塊,進行強化實踐。

至於小冊爲何叫進階實踐指南,由於在講解進階玩法的同時,也包含了不少實踐的小demo。還有一些面試中的問答環節,讓讀者從面試上脫穎而出。

目前小冊已經上線,這裏有 2 個 7 折的優惠碼奉上,先到先得。

  • 小冊 7 折優惠碼: cRftnJvJ
  • 小冊 7 折優惠碼: 5EPxuNV5
相關文章
相關標籤/搜索