redux架构基础
- 2020 年 1 月 2 日
- 筆記
。


本文书接 从flux到redux , 是《深入浅出react和redux》为主的比较阅读笔记。
redux架构基础
“如果你愿意限制做事方式的灵活度,你几乎总会发现可以做得更好。”——John Carmark
redux的官方定义是:
Redux is a predictable static container for JavaScript apps.
按照作者Dan Abramov的说法,Redux名字的含义是Reducer+Flux。Reducer不是一个Redux特定的术语,而是一个计算机科学中的通用概念,很多语言和框架都有对Reducer函数的支持。就以JavaScript为例,数组类型就有reduce函数,接受的参数就是一个reducer,reduce做的事情就是把数组所有元素依次做“规约”,对每个元素都调用一次参数reducer,通过reducer函数完成规约所有元素的功能
笔者的理解是:redux既不操作dom,也不践行MVC,而是专注于状态管理。它就是一个体积很小且优雅的,规定了使用模式的库。
和flux一样,redux和react也没有必然的联系。redux是flux设计哲学的又一种实现。
redux的哲学思想
single source of trues
"真相,单一的真相"
无论是计数器,还是一个什么牛逼哄哄的聊天软件,整个应用的的状态来源于一个唯一的store
,(sotore.getState)。
Redux并没有阻止一个应用拥有多个Store,只是,在Redux的框架下,让一个应用拥有多个Store不会带来任何好处,最后还不如使用一个Store更容易组织代码。这个唯一Store上的状态,是一个树形的对象,每个组件往往只是用树形对象上一部分的数据,而如何设计Store上状态的结构,就是Redux应用的核心问题。
state is readonly
"状态,只读的状态"
这条哲学不是让你如何去塑造一个"不可写"的state,而是告诉你,必须通过派发(dispatch)一个action的方法改变状态:
let aaa=store.getState();aaa.bbb='ccc';
以上是错误的示范。
那么派发action怎么就能改变state呢?
changes are made with pure function called reducer
"改变,用reducer"
也就是说,action派发之后,响应的事件将被reducer所响应。reducer处理了逻辑之后,store.getState
拿到的状态也随之更新。
现在看来,reduce和action都需要由开发者编写。其中reduce接受两个参数,返回一个全新的状态对象:
const reducer=(preState,action)=>newState;
在《从flux到redux》一文中,我们写了一个注册方法:
// 注册的回调函数包含了业务方法 CounterStore.dispatchToken = Dispatcher.register((action) => { if (action.type === ActionTypes.INCREMENT) { counterValues[action.countrCaption] ++; CounterStore.emitChange(); } else if (action.type === ActionTypes.DECREMENT) { counterValues[action.counterCaption] --; CounterStore.emitChange(); } } );
在redux到表述就是reducer:
const reducer=(preState,action)=>{ const {label,type}=action; switch type(){ case ActionTypes.INCREMENT: return { ...preState, [label]:preState[label]+1; } case ActionTypes.DECREMENT: return { ...preState, [label]:preState[label]-1; } default: return preState } }
所以reduce不负责储存状态,只计算状态。
补白:pure function
函数式编程更在意结果而非过程。JavaScript作为"函数是一等公民"的语言,函数可以是参数,也可以是返回值:
// 面向过程计算1*(1+1)let a=1,b=1,c=1;let d=a+b;d*c; // 函数式编程 1*(1+1)const add=(a,b)=>a+b;const multify=(a,b)=>a*b;multify(1,add(1,1))
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。
纯函数是函数式编程的概念,必须遵守以下一些约束。
•不得改写参数•不能调用系统 I/O 的API•不能调用
Date.now()
或者Math.random()
等不纯的方法,因为每次会得到不一样的结果
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。
让我们总结一下,假如你的页面出现一个bug,在本该展现数据a的地方component1,错误出现了数据2,你可以用这个思路来debug:

redux实践
现在用redux来第三次实现计数器。
安装:
npm install --save react-redux
Action
actiontype的定义和flux版本一模一样。action/index的不同在于:
const increment=(label)=>{ return { type:ActionTypes.INCREMENT, label } } const decrement=(label)=>{ return { type:ActionTypes.DECREMENT, label } }
没有了dispatcher:Dispatcher在flux中存在的作用就是把一个action对象分发给多个注册了的Store,因为redux是是单一store,因此无需显式设置dispatcher。
store
Redux库提供的createStore函数,这个函数第一个参数代表更新状态的reducer,第二个参数是状态的初始值。
在Store下新建index.js
import {createStore} from 'redux'; import reducer from './Reducer'; // 初始状态 const counterValues = { firstCount: 0, secoundCount: 0, thirdCount: 0 } var store=createStore(reducer,initValues); export default store;
Ruducer
// reducer处理分发逻辑 import ActionTypes from '../Action/ActionTypes'; export default (state, action) => { const { label } = action; switch (action.type) { case ActionTypes.INCREMENT: return { ...state, [label]: state[label] + 1 }; case ActionTypes.DECREMENT: return { ...state, [label]: state[label] - 1 } default: return state; } }
在reducer中,绝对不能去修改参数中的state。
View
现在,修改所有组件放到src/view文件夹。
在ClickCounter中,我们不再区分不同组件的状态。而是统一向store拿。初始状态可以从store.getState()[this.props.label]
拿。,每个组件往往只需要使用返回状态的一部分数据。为了避免重复代码,我们把从store获得状态的逻辑放在getOwnState函数中,这样任何关联Store状态的地方都可以重用这个函数。
在componentDidMount函数中,我们通过Store的subscribe监听其变化,只要Store状态发生变化,就会调用这个组件的onChange方法;在componentWillUnmount函数中,我们把这个监听注销掉,这个清理动作和componentDidMount中的动作对应。
// view/ClickCounter.js import React, { Component } from 'react' import store from '../stores' import Actions from '../Action/index' const styles = { //... } class ClickCounter extends Component { constructor(props) { super(props) this.state = this.getOwnState(); } getOwnState=()=>{ return { count: store.getState()[this.props.label] } } // 保持store和state的同步 componentDidMount() { store.subscribe(this.onChange); } componentWillUnmount() { store.unsubscribe(this.onChange); } onChange = () => { this.setState(this.getOwnState()); } // 派发 onClickIncrementButton = () => { store.dispatch(Actions.increment(this.props.label)); } onClickDecrementButton = () => { store.dispatch(Actions.decrement(this.props.label)); } render() { return ( <div style={styles.counter}> <div style={styles.label}>{this.props.label}</div> <button onClick={this.onClickDecrementButton}>-</button> <div style={styles.showbox}>{this.state.count}</div> <button onClick={this.onClickIncrementButton}>+</button> </div> ) } } export default ClickCounter;
再来看allCount组件,一开始也是初始化一个getOwnState,通过遍历获取,再通过store.subscribe来绑定事件
import React, { Component } from 'react' import store from '../stores'; export default class extends Component { constructor(props) { super(props); this.state = this.getOwnState(); } getOwnState() { const state = store.getState(); let sum = 0; Object.keys(state).forEach((key) => { if (state.hasOwnProperty(key)) { sum += state[key]; } }); return { sum: sum }; } onChange = () => { this.setState(this.getOwnState()); } shouldComponentUpdate(nextProps, nextState) { return nextState.sum !== this.state.sum; } componentDidMount() { store.subscribe(this.onChange); } componentWillUnmount() { store.unsubscribe(this.onChange); } render() { // switch return ( <div>Total Count: {this.state.sum}</div> ); } }
那么redux版的计数器功能就完成了。
容器与傻瓜
redux版计数器,组件就做两件事:
•跟store拿状态,初始化初始状态•监听store的改变,通过setState更新
这样的设计仍然是违反单一职责原则的。我们应该考虑把组件拆分为嵌套两部分:父组件负责跟store拿状态,它必须有子组件才能运行,是为"容器组件",子组件负责根据props更新界面,是为不用思考的"傻瓜组件"。如下图:

抽离这两部分有两个要点,就是容器组件应当是可复用的,而傻瓜组件不应有半点自身的思考,它是无状态的(可以是函数式组件)。
// 容器组件 class WithContainer extends Component { constructor(props) { super(props) this.state = this.getOwnState(); } getOwnState=()=>{ return { count: store.getState()[this.props.label] } } componentDidMount() { store.subscribe(this.onChange); } componentWillUnmount() { store.unsubscribe(this.onChange); } onChange = () => { this.setState(this.getOwnState()) } onClickIncrementButton = () => { store.dispatch(Actions.increment(this.props.label)); } onClickDecrementButton = () => { store.dispatch(Actions.decrement(this.props.label)); } render() { return ( <ClickCounter label={this.props.label} count={this.state.count} onClickIncrementButton={this.onClickIncrementButton} onClickDecrementButton={this.onClickDecrementButton} /> ) } } export default WithContainer
傻瓜组件就是一个纯函数:
// 傻瓜组件 function ClickCounter(props){ const { label, count, onClickDecrementButton, onClickIncrementButton } = props; return ( <div style={styles.counter}> <div style={styles.label}>{label}</div> <button onClick={onClickDecrementButton}>-</button> <div style={styles.showbox}>{count}</div> <button onClick={onClickIncrementButton}>+</button> </div> ) }
跨代传值解决方案:context
当前所有组件都是单独引入store。写起来很冗余。
一个应用中,最好只有一个地方需要直接导入Store,这个位置当然应该是在调用最顶层React组件的位置。在我们的ControlPanel例子中,就是应用的入口文件src/index.js中,其余组件应该避免直接导入Store。
不让组件直接导入Store,那就只能让组件的上层组件把Store传递下来了。首先想到的当然是用props,毕竟,React组件就是用props来传递父子组件之间的数据的。不过,这种方法有一个很大的缺陷,就是从上到下,所有的组件都要帮助传递这个props。设想在一个嵌套多层的组件结构中,只有最里层的组件才需要使用store,但是为了把store从最外层传递到最里层,就要求中间所有的组件都需要增加对这个storeprop的支持,即使根本不使用它,这无疑很麻烦。
因此就要用到react的跨代传值利器——context。

所谓Context,就是“上下文环境”,让一个树状组件上所有组件都能访问一个共同的对象,为了完成这个任务,需要上级组件和下级组件配合。
首先,上级组件要宣称自己支持context,并且提供一个函数来返回代表Context的对象。然后,这个上级组件之下的所有子孙组件,只要宣称自己需要这个context,就可以通过this.context访问到它。
我们自然想到在应用顶端宣称支持context并把store传入。为此,我们创建一个特殊的组件——Provider。
在src下新建一个Provider.js:
import {Component} from 'react';import PropTypes from 'prop-types'; class Provider extends Component { getChildContext() { return { store: this.props.store }; } render() { return this.props.children; } } Provider.propTypes = { store: PropTypes.object.isRequired} Provider.childContextTypes = { store: PropTypes.object}; export default Provider;
然后在index.js中引入Provider和store:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './view/App'; import * as serviceWorker from './serviceWorker'; import Provider from './view/Provider'; import store from './stores/index'; ReactDOM.render(<Provider store={store}> <App /> </Provider>, document.getElementById('root')); serviceWorker.unregister();
自此,Provider成为了完全意义上的顶层组件。当然,如同我们上面看到的,Provider只是把渲染工作完全交给子组件,它扮演的角色只是提供Context,包住了最顶层的ControlPanel,也就让context覆盖了整个应用中所有组件。
那么底层组件如何获取context呢?当然是修改容器组件。以ClickCount为例:
import React, { Component } from 'react'; import PropTypes from 'prop-types'; // import store from '../stores'; 不再需要引入 import Actions from '../Action/index'; /* 为了让WithContainer能够访问到context,必须给WithContainer类的contextTypes赋值和Provider.childContextTypes一样的值,两者必须一致,不然就无法访问到context, */ WithContainer.contextTypes = { store: PropTypes.object }
然后就可以用this.context.store
来获取store了。
class WithContainer extends Component { /* 在调用super的时候,一定要带上context参数,这样才能让React组件初始化实例中的context,不然组件的其他部分就无法使用this.context。 */ constructor(props, context) { super(props, context) this.state = this.getOwnState(); } getOwnState = () => { return { count: this.context.store.getState()[this.props.label] } } componentDidMount() { this.context.store.subscribe(this.onChange); } componentWillUnmount() { this.context.store.unsubscribe(this.onChange); } onChange = () => { this.setState(this.getOwnState()) } onClickIncrementButton = () => { this.context.store.dispatch(Actions.increment(this.props.label)); } onClickDecrementButton = () => { this.context.store.dispatch(Actions.decrement(this.props.label)); } render() { return ( <ClickCounter label={this.props.label} count={this.state.count} onClickIncrementButton={this.onClickIncrementButton} onClickDecrementButton={this.onClickDecrementButton} /> ) }}
在本文中,我们学习了redux的哲学,从框架原理层面了解了如何用redux来完成React应用,并提供优化方案——第一是把一个组件拆分为容器组件和傻瓜组件,第二是使用React的Context来提供一个所有组件都可以直接访问的Context,也不难发现,这两种方法都有套路,完全可以把套路部分抽取出来复用,这样每个组件的开发只需要关注于不同的部分就可以了。
实际上本文到目前为止,从来没讲什么react-redux。实现的所有思路都是手撸。
实际上,已经有这样的一个库来完成这些工作了,这个库就是react-redux。
终极解决方案:react-redux
首先是安装react-redux:
npm i react-redux -S
redux将实现两个重要的功能:
•connect:链接容器组件和傻瓜组件。•Provider:提供包含store的context
connect
connect相当于一个容器组件的工厂。帮助我们创建了容器它的方法是cxonnect(mapStateToProps, mapDispatchToProps)
,connect是reactredux提供的一个方法,这个方法接收两个参数mapStateToProps和mapDispatchToProps(当无计算时,为非必传),执行结果依然是一个函数,所以才可以在后面又加一个圆括号,把connect函数执行的结果立刻执行,这一次参数是Counter这个傻瓜组件。这里有两次函数执行,第一次是connect函数的执行,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件,mapStateToProps和mapDispatchToProps都可以包含第二个参数,代表ownProps,也就是直接传递给外层容器组件的props。
// 精简后的ClickCounterimport React, { Component } from 'react';import PropTypes from 'prop-types';import Actions from '../Action/index';import { connect } from 'react-redux'; const styles = { // ...} // ownProps也就是直接传递给外层容器组件的props。// 把state转化为属性function mapStateToProps(state, ownProps) { return { count: state[ownProps.label] }}// 定义改变逻辑,需dispatch触发:function mapDispatchToProps(dispatch, ownProps) { return { onClickIncrementButton: () => { dispatch(Actions.increment(ownProps.label)); }, onClickDecrementButton: () => { dispatch(Actions.decrement(ownProps.label)); } }} function ClickCounter(props) { const { label, count, onClickDecrementButton , onClickIncrementButton } = props; return ( <div style={styles.counter}> <div style={styles.label}>{label}</div> <button onClick={onClickDecrementButton}>-</button> <div style={styles.showbox}>{count}</div> <button onClick={onClickIncrementButton}>+</button> </div> )} export default connect(mapStateToProps, mapDispatchToProps)(ClickCounter);
在AllCounter中,写法也是大大简化
import React from 'react'import {connect} from 'react-redux'; function AllCount({ sum }) { return <div>Total Count: {sum}</div>} function mapStateToProps(state, ownProps) { let sum = 0; Object.keys(state).forEach((key) => { if (state.hasOwnProperty(key)) { sum += state[key]; } }); return { sum: sum };} export default connect(mapStateToProps)(AllCount);
Provider
Provider的用法和之前定义的几乎一致,而且不必再定义默认数据类型了:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './view/App'; import * as serviceWorker from './serviceWorker'; // import Provider from './view/Provider'; 不再用自己造的轮子 import {Provider} from 'react-redux'; import store from './stores/index'; ReactDOM.render(<Provider store={store}> <App /> </Provider>, document.getElementById('root')); serviceWorker.unregister();
Redux是Flux框架的一个巨大改进,Redux强调单一数据源、保持状态只读和数据改变只能通过纯函数完成的基本原则,和React的UI=render(state)思想完全契合。我们在这一章中用不同方法,循序渐进的改进了计数器,为的就是更清晰地理解每个改进背后的动因,最后,我们终于通过react-redux完成了React和Redux的融合。