React 中的 Redux 詳解:
Redux 安裝指令是:
> yarn add redux react-redux
Redux 中的核心是:
store是應用的狀態管理中心,儲存著是應用的狀態(state),當收到狀態的更新時,會觸發視覺元件進行更新。
container是視覺元件的容器,負責把傳入的狀態變數渲染成視覺元件,在瀏覽器顯示出來。
reducer是動作(action)的處理中心, 負責處理各種動作併產生新的狀態(state),返回給store。
Redux中的工作流程是:
- 使用函式createStore建立store資料點
- 建立Reducer。它要改變的元件,它獲取state和action,生成新的state
- 用subscribe監聽每次修改情況
- dispatch執行,reducer(currentState,action)處理當前dispatch後的傳入的action.type並返回給currentState處理後的state,通過currentListeners.forEach(v=>v())執行監聽函式,並最後返回當前 action狀態
實現Redux:
getState實現
1 export const createStore = () => { 2 let currentState = {} // 公共狀態 3 function getState() { //getter 4 return currentState 5 } 6 function dispatch() {} // setter 7 function subscribe() {} // 釋出訂閱 8 return { getState, dispatch, subscribe } 9 }
dispatch實現:
>
但是dispatch()
的實現我們得思考一下,經過上面的分析,我們的目標是有條件地、具名地修改store的資料,那麼我們要如何實現這兩點呢?我們已經知道,在使用dispatch的時候,我們會給dispatch()傳入一個action物件,這個物件包括我們要修改的state以及這個操作的名字(actionType),根據type的不同,store會修改對應的state。我們這裡也沿用這種設計:
>
1export const createStore = () => { 2 let currentState = {} 3 function getState() { 4 return currentState 5 } 6 function dispatch(action) { 7 switch (action.type) { 8 case 'plus': 9 currentState = { 10 ...state, 11 count: currentState.count + 1 12 } 13 } 14 } 15 function subscribe() {} 16 return { getState, subscribe, dispatch } 17 }
>
我們把對actionType的判斷寫在了dispatch中,這樣顯得很臃腫,也很笨拙,於是我們想到把這部分修改state的規則抽離出來放到外面,這就是我們熟悉的reducer
。我們修改一下程式碼,讓reducer從外部傳入:
>
1 import { reducer } from './reducer' 2 export const createStore = (reducer) => { 3 let currentState = {} 4 function getState() { 5 return currentState 6 } 7 function dispatch(action) { 8 currentState = reducer(currentState, action) 9 } 10 function subscribe() {} 11 return { getState, dispatch, subscribe } 12 }
>
然後我們建立一個reducer.js檔案,寫我們的reducer
>
1 //reducer.js 2 const initialState = { 3 count: 0 4 } 5 export function reducer(state = initialState, action) { 6 switch(action.type) { 7 case 'plus': 8 return { 9 ...state, 10 count: state.count + 1 11 } 12 case 'subtract': 13 return { 14 ...state, 15 count: state.count - 1 16 } 17 default: 18 return initialState 19 } 20 }
>
程式碼寫到這裡,我們可以驗證一下getState
和dispatch
:
>
1 //store.js 2 import { reducer } from './reducer' 3 export const createStore = (reducer) => { 4 let currentState = {} 5 function getState() { 6 return currentState 7 } 8 function dispatch(action) { 9 currentState = reducer(currentState, action) 10 } 11 function subscribe() {} 12 return { getState, subscribe, dispatch } 13 } 14 15 const store = createStore(reducer) //建立store 16 store.dispatch({ type: 'plus' }) //執行加法操作,給count加1 17 console.log(store.getState()) //獲取state
>
執行程式碼,我們會發現,列印得到的state是:{count: NaN },這是由於store裡初始資料為空,state.count + 1實際上是underfind+1,輸出了NaN,所以我們得先進行store資料初始化,我們在執行dispatch({ type: 'plus' })之前先進行一次初始化的dispatch,這個dispatch的actionType可以隨便填,只要不和已有的type重複,讓reducer裡的switch能走到default去初始化store就行了: >1 import { reducer } from './reducer' 2 export const createStore = (reducer) => { 3 let currentState = {} 4 function getState() { 5 return currentState 6 } 7 function dispatch(action) { 8 currentState = reducer(currentState, action) 9 } 10 function subscribe() {} 11 dispatch({ type: '@@REDUX_INIT' }) //初始化store資料 12 return { getState, subscribe, dispatch } 13 } 14 15 const store = createStore(reducer) //建立store 16 store.dispatch({ type: 'plus' }) //執行加法操作,給count加1 17 console.log(store.getState()) //獲取state
>
執行程式碼,我們就能列印到的正確的state:{ count: 1 }
>
subscribe實現
>
儘管我們已經能夠存取公用state,但store的變化並不會直接引起檢視的更新,我們需要監聽store的變化,這裡我們應用一個設計模式——觀察者模式,觀察者模式被廣泛運用於監聽事件實現(有些地方寫的是釋出訂閱模式,但我個人認為這裡稱為觀察者模式更準確,有關觀察者和釋出訂閱的區別,討論有很多,讀者可以搜一下)
所謂觀察者模式,概念也很簡單:觀察者監聽被觀察者的變化,被觀察者發生改變時,通知所有的觀察者。那麼我們如何實現這種監聽-通知的功能呢,為了照顧還不熟悉觀察者模式實現的同學,我們先跳出redux,寫一段簡單的觀察者模式實現程式碼:
>
1 //觀察者 2 class Observer { 3 constructor (fn) { 4 this.update = fn 5 } 6 } 7 //被觀察者 8 class Subject { 9 constructor() { 10 this.observers = [] //觀察者佇列 11 } 12 addObserver(observer) { 13 this.observers.push(observer)//往觀察者佇列新增觀察者 14 } 15 notify() { //通知所有觀察者,實際上是把觀察者的update()都執行了一遍 16 this.observers.forEach(observer => { 17 observer.update() //依次取出觀察者,並執行觀察者的update方法 18 }) 19 } 20 } 21 22 var subject = new Subject() //被觀察者 23 const update = () => {console.log('被觀察者發出通知')} //收到廣播時要執行的方法 24 var ob1 = new Observer(update) //觀察者1 25 var ob2 = new Observer(update) //觀察者2 26 subject.addObserver(ob1) //觀察者1訂閱subject的通知 27 subject.addObserver(ob2) //觀察者2訂閱subject的通知 28 subject.notify() //發出廣播,執行所有觀察者的update方法
>
解釋一下上面的程式碼:觀察者物件有一個update
方法(收到通知後要執行的方法),我們想要在被觀察者發出通知後,執行該方法;被觀察者擁有addObserver
和notify
方法,addObserver用於收集觀察者,其實就是將觀察者們的update方法加入一個佇列,而當notify被執行的時候,就從佇列中取出所有觀察者的update方法並執行,這樣就實現了通知的功能。我們redux的監聽-通知功能也將按照這種實現思路來實現subscribe:
有了上面觀察者模式的例子,subscribe的實現應該很好理解,這裡把dispatch和notify做了合併,我們每次dispatch,都進行廣播,通知元件store的狀態發生了變更。
>
1 import { reducer } from './reducer' 2 export const createStore = (reducer) => { 3 let currentState = {} 4 let observers = [] //觀察者佇列 5 function getState() { 6 return currentState 7 } 8 function dispatch(action) { 9 currentState = reducer(currentState, action) 10 observers.forEach(fn => fn()) 11 } 12 function subscribe(fn) { 13 observers.push(fn) 14 } 15 dispatch({ type: '@@REDUX_INIT' }) //初始化store資料 16 return { getState, subscribe, dispatch } 17 }
>
我們來試一下這個subscribe(這裡就不建立元件再引入store再subscribe了,直接在store.js中模擬一下兩個元件使用subscribe訂閱store變化):
>
1 import { reducer } from './reducer' 2 export const createStore = (reducer) => { 3 let currentState = {} 4 let observers = [] //觀察者佇列 5 function getState() { 6 return currentState 7 } 8 function dispatch(action) { 9 currentState = reducer(currentState, action) 10 observers.forEach(fn => fn()) 11 } 12 function subscribe(fn) { 13 observers.push(fn) 14 } 15 dispatch({ type: '@@REDUX_INIT' }) //初始化store資料 16 return { getState, subscribe, dispatch } 17 } 18 19 const store = createStore(reducer) //建立store 20 store.subscribe(() => { console.log('元件1收到store的通知') }) 21 store.subscribe(() => { console.log('元件2收到store的通知') }) 22 store.dispatch({ type: 'plus' }) //執行dispatch,觸發store的通知
>
控制檯成功輸出store.subscribe()傳入的回撥的執行結果:
>
>
到這裡,一個簡單的redux就已經完成,在redux真正的原始碼中還加入了入參校驗等細節,但總體思路和上面的基本相同。
我們已經可以在元件裡引入store進行狀態的存取以及訂閱store變化,數一下,正好十行程式碼(`∀´)Ψ。但是我們看一眼右邊的進度條,就會發現事情並不簡單,篇幅到這裡才過了三分之一。儘管說我們已經實現了redux,但coder們並不滿足於此,我們在使用store時,需要在每個元件中引入store,然後getState,然後dispatch,還有subscribe,程式碼比較冗餘,我們需要合併一些重複操作,而其中一種簡化合並的方案,就是我們熟悉的react-redux。
>react-redux的實現:
----------------------------------------------------------------------------------------
>
上文我們說到,一個元件如果想從store存取公用狀態,需要進行四步操作:import引入store、getState獲取狀態、dispatch修改狀態、subscribe訂閱更新,程式碼相對冗餘,我們想要合併一些重複的操作,而react-redux就提供了一種合併操作的方案:react-redux提供Provider
和connect
兩個API,Provider將store放進this.context裡,省去了import這一步,connect將getState、dispatch合併進了this.props,並自動訂閱更新,簡化了另外三步,下面我們來看一下如何實現這兩個API:
>
Provider實現:
>
我們先從比較簡單的Provider
開始實現,Provider是一個元件,接收store並放進全域性的context
物件,至於為什麼要放進context,後面我們實現connect的時候就會明白。下面我們建立Provider元件,並把store放進context裡
>
1 import React from 'react' 2 import PropTypes from 'prop-types' 3 export class Provider extends React.Component { 4 // 需要宣告靜態屬性childContextTypes來指定context物件的屬性,是context的固定寫法 5 static childContextTypes = { 6 store: PropTypes.object 7 } 8 9 // 實現getChildContext方法,返回context物件,也是固定寫法 10 getChildContext() { 11 return { store: this.store } 12 } 13 14 constructor(props, context) { 15 super(props, context) 16 this.store = props.store 17 } 18 19 // 渲染被Provider包裹的元件 20 render() { 21 return this.props.children 22 } 23 }
>
完成Provider後,我們就能在元件中通過this.context.store這樣的形式取到store,不需要再單獨import store。
>
connect實現:
>
下面我們來思考一下如何實現connect
,我們先回顧一下connect的使用方法:
>
1 export function connect(mapStateToProps, mapDispatchToProps) { 2 return function(Component) { 3 class Connect extends React.Component { 4 componentDidMount() { 5 //從context獲取store並訂閱更新 6 this.context.store.subscribe(this.handleStoreChange.bind(this)); 7 } 8 handleStoreChange() { 9 // 觸發更新 10 // 觸發的方法有多種,這裡為了簡潔起見,直接forceUpdate強制更新,讀者也可以通過setState來觸發子元件更新 11 this.forceUpdate() 12 } 13 render() { 14 return ( 15 <Component 16 // 傳入該元件的props,需要由connect這個高階元件原樣傳回原元件 17 { ...this.props } 18 // 根據mapStateToProps把state掛到this.props上 19 { ...mapStateToProps(this.context.store.getState()) } 20 // 根據mapDispatchToProps把dispatch(action)掛到this.props上 21 { ...mapDispatchToProps(this.context.store.dispatch) } 22 /> 23 ) 24 } 25 } 26 //接收context的固定寫法 27 Connect.contextTypes = { 28 store: PropTypes.object 29 } 30 return Connect 31 } 32 }
>
寫完了connect的程式碼,我們有兩點需要解釋一下:
1. Provider的意義:我們審視一下connect的程式碼,其實context不過是給connect提供了獲取store的途徑,我們在connect中直接import store完全可以取代context。那麼Provider存在的意義是什麼,其實筆者也想過一陣子,後來才想起...上面這個connect是自己寫的,當然可以直接import store,但react-redux的connect是封裝的,對外只提供api,所以需要讓Provider傳入store。
2. connect中的裝飾器模式:回顧一下connect的呼叫方式:connect(mapStateToProps, mapDispatchToProps)(App)
其實connect完全可以把App跟著mapStateToProps一起傳進去,看似沒必要return一個函式再傳入App,為什麼react-redux要這樣設計,react-redux作為一個被廣泛使用的模組,其設計肯定有它的深意。
其實connect這種設計,是裝飾器模式的實現,所謂裝飾器模式,簡單地說就是對類的一個包裝,動態地拓展類的功能。connect以及React中的高階元件(HoC)都是這一模式的實現
>1 //普通connect使用 2 class App extends React.Component{ 3 render(){ 4 return <div>hello</div> 5 } 6 } 7 function mapStateToProps(state){ 8 return state.main 9 } 10 function mapDispatchToProps(dispatch){ 11 return bindActionCreators(action,dispatch) 12 } 13 export default connect(mapStateToProps,mapDispatchToProps)(App)
1 //使用裝飾器簡化 2 @connect( 3 state=>state.main, 4 dispatch=>bindActionCreators(action,dispatch) 5 ) 6 class App extends React.Component{ 7 render(){ 8 return <div>hello</div> 9 } 10 }
>
寫完了react-redux,我們可以寫個demo來測試一下:使用create-react-app
建立一個專案,刪掉無用的檔案,並建立store.js、reducer.js、react-redux.js來分別寫我們redux和react-redux的程式碼,index.js是專案的入口檔案,在App.js中我們簡單的寫一個計數器,點選按鈕就派發一個dispatch,讓store中的count加一,頁面上顯示這個count。最後檔案目錄和程式碼如下:
>
1 // store.js 2 export const createStore = (reducer) => { 3 let currentState = {} 4 let observers = [] //觀察者佇列 5 function getState() { 6 return currentState 7 } 8 function dispatch(action) { 9 currentState = reducer(currentState, action) 10 observers.forEach(fn => fn()) 11 } 12 function subscribe(fn) { 13 observers.push(fn) 14 } 15 dispatch({ type: '@@REDUX_INIT' }) //初始化store資料 16 return { getState, subscribe, dispatch } 17 }
1 //reducer.js 2 const initialState = { 3 count: 0 4 } 5 6 export function reducer(state = initialState, action) { 7 switch(action.type) { 8 case 'plus': 9 return { 10 ...state, 11 count: state.count + 1 12 } 13 case 'subtract': 14 return { 15 ...state, 16 count: state.count - 1 17 } 18 default: 19 return initialState 20 } 21 }
1 //react-redux.js 2 import React from 'react' 3 import PropTypes from 'prop-types' 4 export class Provider extends React.Component { 5 // 需要宣告靜態屬性childContextTypes來指定context物件的屬性,是context的固定寫法 6 static childContextTypes = { 7 store: PropTypes.object 8 } 9 10 // 實現getChildContext方法,返回context物件,也是固定寫法 11 getChildContext() { 12 return { store: this.store } 13 } 14 15 constructor(props, context) { 16 super(props, context) 17 this.store = props.store 18 } 19 20 // 渲染被Provider包裹的元件 21 render() { 22 return this.props.children 23 } 24 } 25 26 export function connect(mapStateToProps, mapDispatchToProps) { 27 return function(Component) { 28 class Connect extends React.Component { 29 componentDidMount() { //從context獲取store並訂閱更新 30 this.context.store.subscribe(this.handleStoreChange.bind(this)); 31 } 32 handleStoreChange() { 33 // 觸發更新 34 // 觸發的方法有多種,這裡為了簡潔起見,直接forceUpdate強制更新,讀者也可以通過setState來觸發子元件更新 35 this.forceUpdate() 36 } 37 render() { 38 return ( 39 <Component 40 // 傳入該元件的props,需要由connect這個高階元件原樣傳回原元件 41 { ...this.props } 42 // 根據mapStateToProps把state掛到this.props上 43 { ...mapStateToProps(this.context.store.getState()) } 44 // 根據mapDispatchToProps把dispatch(action)掛到this.props上 45 { ...mapDispatchToProps(this.context.store.dispatch) } 46 /> 47 ) 48 } 49 } 50 51 //接收context的固定寫法 52 Connect.contextTypes = { 53 store: PropTypes.object 54 } 55 return Connect 56 } 57 }
1 //index.js 2 import React from 'react' 3 import ReactDOM from 'react-dom' 4 import App from './App' 5 import { Provider } from './react-redux' 6 import { createStore } from './store' 7 import { reducer } from './reducer' 8 9 ReactDOM.render( 10 <Provider store={createStore(reducer)}> 11 <App /> 12 </Provider>, 13 document.getElementById('root') 14 );
1 //App.js 2 import React from 'react' 3 import { connect } from './react-redux' 4 5 const addCountAction = { 6 type: 'plus' 7 } 8 9 const mapStateToProps = state => { 10 return { 11 count: state.count 12 } 13 } 14 15 const mapDispatchToProps = dispatch => { 16 return { 17 addCount: () => { 18 dispatch(addCountAction) 19 } 20 } 21 } 22 23 class App extends React.Component { 24 render() { 25 return ( 26 <div className="App"> 27 { this.props.count } 28 <button onClick={ () => this.props.addCount() }>增加</button> 29 </div> 30 ); 31 } 32 } 33 34 export default connect(mapStateToProps, mapDispatchToProps)(App)執行專案,點選增加按鈕,能夠正確的計數,OK大成功,我們整個redux、react-redux的流程就走通了