「React進階」只用兩個自定義 Hooks 就能替代 React-Redux ?
本文轉載自微信公眾號「前端Sharing」,作者前端Sharing。轉載本文請聯系前端Sharing公眾號。
前言
之前有朋友問我,React Hooks 能否解決 React 項目狀態管理的問題。這個問題讓我思索了很久,最后得出的結論是:能,不過需要兩個自定義 hooks 去實現。那么具體如何實現的呢?那就是今天要講的內容了。
通過本文,你能夠學習以下內容:
- useContext ,useRef ,useMemo,useEffect 的基本用法。
- 如何將不同組件的自定義 hooks 建立通信,共享狀態。
- 合理編寫自定義 hooks , 分析 hooks 之間的依賴關系。
- 自定義 hooks 編寫過程中一些細節問題。
帶著如上的知識點,開啟閱讀之旅吧~
一 設計思路
首先,看一下要實現的兩個自定義 hooks 具體功能。
- useCreateStore 用于產生一個狀態 Store ,通過 context 上下文傳遞 ,為了讓每一個自定義 hooks useConnect 都能獲取 context 里面的狀態屬性。
- useConnect 使用這個自定義 hooks 的組件,可以獲取改變狀態的 dispatch 方法,還可以訂閱 state ,被訂閱的 state 發生變化,組件更新。
如何讓不同組件的自定義 hooks 共享狀態并實現通信呢?
首先不同組件的自定義 hooks ,可以通過 useContext 獲得共有狀態,而且還需要實現狀態管理和組件通信,那么就需要一個狀態調度中心來統一做這些事,可以稱之為 ReduxHooksStore ,它具體做的事情如下:
- 全局管理 state, state 變化,通知對應組件更新。
- 收集使用 useConnect 組件的信息。組件銷毀還要清除這些信息。
- 維護并傳遞負責更新的 dispatch 方法。
- 一些重要 api 要暴露給 context 上下文,傳遞給每一個 useConnect。
1 useCreateStore 設計
首先 useCreateStore 是在靠近根部組件的位置的, 而且全局只需要一個,目的就是創建一個 Store ,并通過 Provider 傳遞下去。
使用:
- const store = useCreateStore( reducer , initState )
參數:
- 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 原理圖
二 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 主要做的是:
- 接收 reducer 和 initState ,通過 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 清空狀態。
效果
六 總結
本文通過兩個自定義 hooks 實現了 React-Redux 的基本功能,這個模式在真實項目中可以使用嗎?我覺得如果是小型項目,是完全可以使用的,對于大型項目還是用 React Redux 或者其他成熟的狀態管理工具。