1. 程式人生 > 實用技巧 >React 中的 Redux 詳解:

React 中的 Redux 詳解:

Redux 安裝指令是:

> yarn add redux react-redux

Redux 中的核心是:

store是應用的狀態管理中心,儲存著是應用的狀態(state),當收到狀態的更新時,會觸發視覺元件進行更新。

container是視覺元件的容器,負責把傳入的狀態變數渲染成視覺元件,在瀏覽器顯示出來。

reducer是動作(action)的處理中心, 負責處理各種動作併產生新的狀態(state),返回給store。

Redux中的工作流程是:

  1. 使用函式createStore建立store資料點
  2. 建立Reducer。它要改變的元件,它獲取state和action,生成新的state
  3. subscribe監聽每次修改情況
  4. 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。我們這裡也沿用這種設計: >
 1
export 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 }

>

程式碼寫到這裡,我們可以驗證一下getStatedispatch

>

 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方法(收到通知後要執行的方法),我們想要在被觀察者發出通知後,執行該方法;被觀察者擁有addObservernotify方法,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提供Providerconnect兩個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的使用方法:

我們已經知道,connect接收mapStateToProps、mapDispatchToProps兩個方法,然後返回一個高階函式,這個高階函式接收一個元件,返回一個高階元件(其實就是給傳入的元件增加一些屬性和功能)connect根據傳入的map,將state和dispatch(action)掛載子元件的props上,我們直接放出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的流程就走通了