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的流程就走通了
 

 

posted @ 2020-07-18 20:51  柚子小哥哥  阅读(3673)  评论(0编辑  收藏  举报