以前有朋友問我,React Hooks 可否解決 React 項目狀態管理的問題。這個問題讓我思索了好久,最後得出的結論是:能,不過須要兩個自定義 hooks 去實現。那麼具體如何實現的呢? 那就是今天要講的內容了。前端
經過本文,你可以學習如下內容:react
帶着如上的知識點,開啓閱讀之旅吧~(創做不易,但願屏幕前的你能給筆者賞個贊,以此鼓勵我繼續創做前端硬文。)面試
首先,看一下要實現的兩個自定義 hooks 具體功能。redux
useCreateStore
用於產生一個狀態 Store ,經過 context 上下文傳遞 ,爲了讓每個自定義 hooks useConnect
都能獲取 context 裏面的狀態屬性。useConnect
使用這個自定義 hooks 的組件,能夠獲取改變狀態的 dispatch 方法,還能夠訂閱 state ,被訂閱的 state 發生變化,組件更新。如何讓不一樣組件的自定義 hooks 共享狀態並實現通訊呢?api
首先不一樣組件的自定義 hooks ,能夠經過 useContext
得到共有狀態,並且還須要實現狀態管理和組件通訊,那麼就須要一個狀態調度中心來統一作這些事,能夠稱之爲 ReduxHooksStore
,它具體作的事情以下:數組
useConnect
組件的信息。組件銷燬還要清除這些信息。dispatch
方法。useConnect
。首先 useCreateStore
是在靠近根部組件的位置的, 並且全局只須要一個,目的就是建立一個 Store
,並經過 Provider
傳遞下去。緩存
使用:markdown
const store = useCreateStore( reducer , initState )
複製代碼
參數:dom
reducer
:全局 reducer,純函數,傳入 state 和 action ,返回新的 state 。initState
: 初始化 state 。返回值:爲 store 暴露的主要功能函數。異步
Store 爲上述所說的調度中心,接收全局 reducer ,內部維護狀態 state ,負責通知更新 ,收集用 useConnect 的組件。
const Store = new ReduxHooksStore(reducer,initState).exportStore()
複製代碼
參數:接收兩個參數,透傳 useCreateStore 的參數。
使用 useConnect 的組件,將得到 dispatch 函數,用於更新 state ,還能夠經過第一個參數訂閱 state ,被訂閱的 state 改變 ,會讓組件更新。
// 訂閱 state 中的 number
const mapStoreToState = (state)=>({ number: state.number })
const [ state , dispatch ] = useConnect(mapStoreToState)
複製代碼
參數:
mapStoreToState
:將 Store 中 state ,映射到組件的 state 中,能夠作視圖渲染使用。dispatch
函數,不會訂閱 state 變化帶來的更新。返回值:返回值是一個數組。
dispatch
函數。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 。
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 是整個功能的核心部分,它要作的事情是獲取最新的 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 ]
}
複製代碼
初始化
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 有一些侷限性,好比:
接下來就是驗證效果環節,我模擬了組件通訊的場景。
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根組件
{ mesA:'111',mesB:'111' }
Index組件
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>
}
複製代碼
mes1 ,mes2
屬性上。本文經過兩個自定義 hooks 實現了 React-Redux 的基本功能,這個模式在真實項目中可使用嗎? 我以爲若是是小型項目,是徹底可使用的,對於大型項目仍是用 React Redux 或者其餘成熟的狀態管理工具。
今天給你們推薦一本掘金小冊 《React進階實踐指南》,本文中的自定義 hooks 也是小冊自定義 hooks 章節中的一個案例。小冊還有不少自定義 hooks 設計案例,並且自定義 hooks 設計和實踐章節都在持續更新維護中,匯聚了筆者多年的心血,感興趣的同窗能夠了解如下,下面是小冊的介紹。
本小冊從基礎進階篇,優化進階篇,原理進階篇,生態進階篇,實踐進階篇,五個方向詳細探討 React 使用指南 和 原理介紹。
至於小冊爲何叫進階實踐指南,由於在講解進階玩法的同時,也包含了不少實踐的小demo。還有一些面試中的問答環節,讓讀者從面試上脫穎而出。
目前小冊已經上線,這裏有 2 個 7 折的優惠碼奉上,先到先得。