redux架构基础

2020-01-03 15:06:33 浏览数 (1)

本文书接 从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:

代码语言:javascript复制
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了。

代码语言:javascript复制
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:

代码语言:javascript复制
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。

代码语言:javascript复制
// 精简后的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的融合。

0 人点赞