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。我们这里也沿用这种设计: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 }
>
代码写到这里,我们可以验证一下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
>
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 }
>
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)