前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >redux架构基础

redux架构基础

作者头像
一粒小麦
修改2020-01-03 15:06:33
1.2K0
修改2020-01-03 15:06:33
举报
文章被收录于专栏:一Li小麦一Li小麦

本文书接 从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的融合。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • redux架构基础
    • redux的哲学思想
      • single source of trues
      • state is readonly
      • changes are made with pure function called reducer
      • 补白:pure function
    • redux实践
      • Action
      • store
      • Ruducer
      • View
    • 容器与傻瓜
      • 跨代传值解决方案:context
        • 终极解决方案:react-redux
          • connect
          • Provider
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档