本文书接 从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的方法改变状态:
代码语言:javascript复制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接受两个参数,返回一个全新的状态对象:
代码语言:javascript复制const reducer=(preState,action)=>newState;
在《从flux到redux》一文中,我们写了一个注册方法:
代码语言:javascript复制// 注册的回调函数包含了业务方法
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:
代码语言:javascript复制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来第三次实现计数器。
安装:
代码语言:javascript复制npm install --save react-redux
Action
actiontype的定义和flux版本一模一样。action/index的不同在于:
代码语言:javascript复制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
代码语言:javascript复制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
代码语言:javascript复制
// 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中的动作对应。
代码语言:javascript复制// 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来绑定事件
代码语言:javascript复制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更新界面,是为不用思考的"傻瓜组件"。如下图:
抽离这两部分有两个要点,就是容器组件应当是可复用的,而傻瓜组件不应有半点自身的思考,它是无状态的(可以是函数式组件)。
代码语言:javascript复制// 容器组件
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
傻瓜组件就是一个纯函数:
代码语言:javascript复制// 傻瓜组件
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:
代码语言:javascript复制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为例:
代码语言:javascript复制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中,写法也是大大简化
代码语言:javascript复制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的用法和之前定义的几乎一致,而且不必再定义默认数据类型了:
代码语言:javascript复制
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的融合。