<深入浅出React和Redux> - 从Flux到Redux

杨旭 bio photo By 杨旭

从Flux到Redux

针对React自己来管理数据的问题, 将使用Flux或Redux来管理数据

Flux

Flux是React官方推出的数据管理框架, 如果说React用来替代jQuery, 那么Flux就是用来替换Backbone, Ember等MVC框架的

其核心思想是单向数据流

MVC框架的缺陷

  • Model 负责管理数据, 大部分业务逻辑应该位于Model中
  • View 负责渲染用户界面, 应该避免在View中包含业务逻辑
  • Controller 负责接受用户输入, 并调用Model中的业务逻辑, 把结果交给View进行渲染

但是, MVC真的很快就会变得非常复杂, 不同模块之间复杂的依赖关系, 会导致系统脆弱而且不可预测

在实际使用的时候, Model和View很容易形成复杂的调用关系:

原因就是View和Model始终存在, 开发者为了便利, 往往忽视理想的数据流规则, 直接让两者进行对话, 从而导致了上图中复杂的依赖关系.

Flux就是一个更严格的数据流控制

  • Dispatcher 处理动作分发, 维持Store之间的依赖
  • Store 负责存储数据和处理数据相关的逻辑
  • Action 驱动Dispatcher的对象
  • View 视图部分, 负责显示用户界面

在使用Flux的时候, 每增加一个新的功能, 并不需要新增一个Dispatcher, 只需要增加一种新的Action类型, Dispatcher对外的接口并不改变

Flux应用

首先进行安装:

$ yarn add flux

Dispatcher

几乎所有应用都只有一个Dispatcher

创建AppDispatcher.js声明唯一的对象, 其他代码使用的时候, 只需要引用他, 它的作用就是派发action

import { Dispatcher } from 'flux';

export default new Dispatcher();

action

表示一个动作

  • 一个纯粹的数据对象
  • 必须包含一个type字段, 代表动作类型, 应该是字符串类型

定义action通常需要两个文件:

  • 一个定义action的类型
  • 一个定义action的构造函数

分成两个文件的原因是: Store中会针对不同类型进行不同操作, 会单独引用类型文件

在Actions.js中, 借助Dispatcher对象分发不同类型的action

// ActionTypes.js
export const INCREASE = 'increase';
export const DECREASE = 'decrease';

// Actions.js
import * as ActionTypes from './ActionTypes';
import AppDispatcher from './AppDispatcher';

export const increase = (caption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.INCREASE,
    caption: caption
  })
}

export const decrease = (caption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.DECREASE,
    caption: caption
  })
}

Store

也是一个对象, 用来存储应用状态, 同时接受Dispatcher派发的动作, 由此来决定是否需要更新应用状态

使用Flux之后会带来一个弊端: 文件数量大大增加, 所以需要使用单独的stores来存储所有的Store文件

  • CounterStore.js
const counterValues = {
  'First': 0,
  'Second': 10,
  'Thired': 100
}

const CounterStore = Object.assign({}, EventEmitter.prototype, {
  getCounterValues: () => {
    return counterValues
  },
  emitChange: () => {
    this.emit(CHANGE_EVENT)
  },
  addChangeListener: (callback) => {
    this.on(CHANGE_EVENT, callback)
  },
  removeChangeListener: (callback) => {
    this.removeListener(CHANGE_EVENT, callback)
  }
})

当Store状态发生变化的时候, 使用消息的方式建立Store和View之间的联系, 更新界面状态

理论上, getCounterValues应该返回一个不可变对象, 这里只是为了便于演示, 没有引入Immutable

Store只有注册到Dispatcher上才能发挥真正的作用

CounterStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREASE) {
    counterValues[action.caption]++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREASE) {
    counterValues[action.caption]--;
    CounterStore.emitChange();
  }
})

register函数会返回一个token值, 用来在store之间的同步

  • SummaryStore.js

function computeSummary(counterValues) {
  let summary = 0
  for (const key in counterValues) {
    if (counterValues.hasOwnProperty(key)) {
      summary += counterValues[key]
    }
  }

  return summary
}

const SummaryStore = Object.assign({}, EventEmitter.prototype, {
  getSummary: () => {
    return computeSummary(CounterStore.getCounterValues())
  }
})

SummaryStore虽然叫做Store, 但是他实际上不存储数据, 调用CounterStore获取数据的方法进行计算, 并返回计算结果

SummaryStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREASE || action.type === ActionTypes.DECREASE) {
    AppDispatcher.waitFor([CounterStore.dispatchToken])
    SummaryStore.emitChange()
  }
})

waitFor暗示了回调函数的执行顺序, 它会等待指定token对应的回调函数执行后才执行.

register只能声明当有任何动作派发时,请调用我, 而不是让Store只监听某些action, 当一个动作被派发的时候, Flux会把所有注册的回调函数都调用一遍, 由回调函数自己判断是否需要处理.

这种设计会让Dispatcher的逻辑最简化

View

首先, View不一定非要使用React, 可以使用任何UI库

在Flux框架下的React组件需要实现以下几个功能:

  • 创建时, 读取Store状态来初始化
  • Store上状态变化时, 同步更新组件状态
  • 如果需要View中更新Store, 只能派发Action

CounterPanel中, 只保留caption的赋值, 其余的属性都会通过Store来获取:

  <div>
    <ClickCounter caption="First" />
    <ClickCounter caption="Second" />
    <ClickCounter caption="Third" />

    <Summary />
  </div>

ClickCounter中通过Store获取初始数据:

this.state = {
  count: CounterStore.getCounterValues()[props.caption]
}

并且绑定Store的变更事件:

  onChange() {
    const newCount = CounterStore.getCounterValues()[this.props.caption];
    this.setState({count: newCount});
  }

  componentDidMount() {
    CounterStore.addChangeListener(this.onChange)
  }

  componentWillUnmount() {
    CounterStore.removeChangeListener(this.onChange)
  }

将+和-按钮的事件切换到helper函数的分发操作:

  increase() {
    Actions.increase(this.props.caption)
  }

  decrease() {
    Actions.decrease(this.props.caption)
  }

完整代码参考:

stoneyangxu/dissecting-react-and-redux

整体的数据流程:

  • 点击View中的按钮, 在increase方法中调用Actions中的函数
  • Actions中的函数调用Dispatcher的方法分发事件
  • Dispatcher广播事件, 并且被CounterStore(或SummaryStore)中register注册的回调获取
  • 回调函数中自行判断是否需要处理该事件, 并操作自身存储的数据, 触发新的事件通知View
  • 新的事件是在组件的生命周期方法中绑定的, 从而获知数据变化并更新显示

Flux的优势

在不使用Flux的时候, 数据都保存在组件内, 每个组件都需要维护自己的数据, 当应用变得复杂的时候, 这种关联难以维护

在Flux架构下, 组件专注于自身的职责渲染组件, 数据都是保存在Store中, 组件只根据事件将数据映射为界面.

Flux中最大的优势就是严格的单向数据流: 想要改变View, 必须通过Store来改变数据状态, 而Store中的数据状态必须通过派发一个action来改变, 这就是规矩

在这个规矩之下, 想追溯一个应用的逻辑就变得非常容易

MVC框架最大的缺点是无法禁绝Model和View之间的通信, 在Flux架构下, Store相当于Model, 而Store只有get方法而没有set方法, 想要修改Store就只能派发action, 这种限制杜绝了混乱的数据流

Flux的不足

  1. Store之间的依赖关系 - 必须使用waitFor来进行控制, 最好的依赖管理是不产生依赖
  2. 难以进行服务端渲染 - 服务端渲染输出的不是DOM而是字符串, flux不是设计出来进行服务端渲染的, 想要实现会很困难
  3. Store混杂了逻辑和状态 - 如果需要动态替换Store, 就只能整体替换, 也就无法保留状态. 在开发时的调试, 或者生产环境下的动态加载, 也就是热加载

Redux

如果把Flux看做一个框架理念的话, Redux就是Flux的一种实现

Redux的基本原则

  • 唯一数据源
  • 保持状态只读
  • 数据改变只能通过纯函数完成

唯一数据源

应用的状态数据保存在唯一的一个Store上

分为多个Store保存造成数据冗余或数据一致性的问题, 使用waitFor进行控制则会导致依赖关系, 使得应用变得复杂.

而这个唯一的Store解决了这些问题, 在Store上的状态是一个树形, 每个组件只使用树形结构上的一部分数据

保持状态只读

改变状态的方法不是去修改状态上的值, 而是创建一个新的状态对象返回给Redux, 由Redux完成新的状态的组装

数据改变只能通过纯函数完成

所谓纯函数就是一个reducer, 根据上一个状态action规约为新的状态

reducer(state, action)

函数的返回结果完全由state和action决定, 并且不产生任何副作用, 也不会修改state和action

function reducer(state, action) => {
    const {counterCaption} = action;
    switch(action.type) {
        case ActionTypes.INCREASE: 
            return {...state, [counterCaption]: state[counterCaption] + 1};
        case ActionTypes.DECREASE: 
            return {...state, [counterCaption]: state[counterCaption] - 1};
        default: 
            return state;
    }
}

“如果你愿意限制做事方式的灵活性, 你几乎总会发现可以做的更好”

Redux实例

先不借助react-redux组件, 以便我们更好的了解Redux的工作方式

$ yarn add redux

ActionTypes.js与Flux版本没有任何区别

export const INCREASE = 'increase';
export const DECREASE = 'decrease';

Actions.js则变得不同, flux中是将Dispatcher分发动作, 而在Redux中则是返回一个action对象:

import * as ActionTypes from './ActionTypes';
import AppDispatcher from './AppDispatcher';

export const increase = (caption) => {
  return {
    type: ActionTypes.INCREASE,
    caption: caption
  }
}

export const decrease = (caption) => {
  return {
    type: ActionTypes.DECREASE,
    caption: caption
  }
}

export default {increase, decrease}

在Redux中没有Dispatcher存在, 在flux中其作用是将action对象分发给多个Store, 而Redux中只有一个Store, 所以将其简化为Store上的dispatch方法

创建一个全局唯一的Store.js:

import { createStore } from 'redux'
import reducer from './Reducer'

const initValues = {
  'First': 0,
  'Second': 10,
  'Third': 100
}

const store = createStore(reducer, initValues)
export default store;

在初始值上只记录三个counter组件的值, Summary的值通过计算得出, 原则是: 避免冗余的数据

创建Reducer.js, 根据action的type字段执行对应的数据操作:

import * as ActionTypes from './ActionTypes'

export default (state, action) => {
  const { caption } = action

  switch (action.type) {
    case ActionTypes.INCREASE:
      return { ...state, [caption]: state[caption] + 1 }
    case ActionTypes.DECREASE:
      return { ...state, [caption]: state[caption] - 1 }
    default:
      return state
  }
}

Redux中将存储state的工作交给框架自身, 让reducer值关心如何更新state, 而不关心怎么存

接着是View部分:

  • 首先的区别是数据来源不同了, 通过唯一的store获取
  • 其次, 通过store对象来订阅事件
  • 更新的时候, 也是通过store对象的dispatch方法来派发动作
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Actions from '../Actions'
import store from '../Store'


class ClickCounter extends Component {

  constructor(props) {
    super(props);

    this.state = this.getOwnState()

    this.onChange = this.onChange.bind(this);
    this.increase = this.increase.bind(this);
    this.decrease = this.decrease.bind(this);
  }

  getOwnState() {
    return {
      value: store.getState()[this.props.caption]
    }
  }

  increase() {
    store.dispatch(Actions.increase(this.props.caption))
  }

  decrease() {
    store.dispatch(Actions.decrease(this.props.caption))
  }

  onChange() {
    this.setState(this.getOwnState());
  }

  componentDidMount() {
    store.subscribe(this.onChange)
  }

  componentWillUnmount() {
    store.unsubscribe(this.onChange)
  }

  render() {
    const { caption } = this.props;
    return (
      <div>
        <button onClick={this.increase}>+</button>
        <button onClick={this.decrease}>-</button>
        <span>{caption} Count: {this.state.value}</span>
      </div>
    );
  }
}

ClickCounter.propTypes = {
  caption: PropTypes.string.isRequired,
  initValue: PropTypes.number,
  onUpdate: PropTypes.func
}

ClickCounter.defaultProps = {
  initValue: 0,
  onUpdate: f => f // 默认是一个什么都不做的函数
}

export default ClickCounter;

容器组件和傻瓜组件

在Redux框架下, React组件通常要完成两项工作:

  • 读取store的状态初始化组件, 监听store的变化刷新组件, 派发action更新store
  • 根据props和state渲染用户界面

根据单一职责原则, 这两个任务是可以拆分的:

  • 容器组件 - 负责和Store打交道
  • 展示组件 - 负责渲染界面, 一个纯函数, 而且不需要有状态, 即无状态组件
import React from 'react';

// 缩略为一个纯函数
function Counter({onIncrease, onDecrease, caption, value}) { // 直接在参数位置解构
  return (
    <div>
      <button onClick={onIncrease}>+</button>
      <button onClick={onDecrease}>-</button>
      <span>{caption} Count: {value}</span>
    </div>
  );
}

export default Counter;

// container

  render() {
    const { caption } = this.props;
    return (
      <Counter caption={caption}
        onIncrease={this.increase}
        onDecrease={this.decrease}
        value={this.state.value} />
    );
  }

组件Context

在上述例子中, 对store的引用使用相对路径:

import store from '../Store'

当项目规模变大, 路径会变得很长, 而且如果将组件独立发布, 根本无法获知store的位置

最好只有一个地方导入store, 就是在React应用的最顶层

当前我们掌握的方式是将store通过props层层传递下来, 这么做的问题就是每一层都要传递这个props

React提供了一个叫做context的功能, 本质上就是一个所有组件都能访问的上下文环境

首先创建一个Provider, 作为一个通用的context提供者:

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.childContextTypes = {
  store: PropTypes.object
}

export default Provider;
  • getChildContext 返回的就是代表context的对象
  • children 是一个特殊属性, 表示之间的组件
  • childrenContextTypes 让React认可它是一个context提供者
  • childContextTypes和getChildContext的属性必须匹配

其次, 在顶层组件使用Provider包裹其他组件:

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  , document.getElementById('root'));

最后, 在底层组件使用store的时候需要进行相应的调整:

  • 定义contextTypes, 与context匹配
CounterContainer.contextTypes = {
  store: PropTypes.object
}
  • 构造函数中, 传入context, 并传入父类的构造函数中
  constructor(props, context) {
    super(props, context);
    // ...
  }
  • 所有使用store的地方, 都通过this.context.store来使用
  getOwnState() {
    return {
      value: this.context.store.getState()[this.props.caption]
    }
  }

React-Redux

在上述例子中, 经过组件拆分提供Context, 可以找到固定的规律, 使用react-redux库来帮助我们自动完成

$ yarn add react-redux

react-redux提供Provider

react-redux要求的Provider要求store不光是一个object, 必须是包含三个函数的object:

  • subscribe
  • dispatch
  • getState
import { Provider } from 'react-redux';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  , document.getElementById('root'));

使用connect连接容器组件和展示组件

语法格式如下, 作用是将无状态组件转换为容器组件

export default connect(mapStateToProps, mapDispatchToProps)(connect)
  • mapStateToProps 将Store上的状态转换为无状态组件的props
  • mapDispatchToProps 将无状态组件的动作转换为派送给Store的动作
import { connect } from 'react-redux';

function mapStateToProps(state, ownProps) {
  return {
    caption: ownProps.caption,
    value: state[ownProps.caption]
  }
}

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrease: () => {
      dispatch(Actions.increase(ownProps.caption))
    },
    onDecrease: () => {
      dispatch(Actions.decrease(ownProps.caption))
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

使用react-redux简化容器后, 可以将容器组件和无状态组件进行合并:

import React from 'react';
import { connect } from 'react-redux';
import Actions from '../Actions'

function Counter({ onIncrease, onDecrease, caption, value }) {
  return (
    <div>
      <button onClick={onIncrease}>+</button>
      <button onClick={onDecrease}>-</button>
      <span>{caption} Count: {value}</span>
    </div>
  );
}

function mapStateToProps(state, ownProps) {
  return {
    caption: ownProps.caption,
    value: state[ownProps.caption]
  }
}

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrease: () => {
      dispatch(Actions.increase(ownProps.caption))
    },
    onDecrease: () => {
      dispatch(Actions.decrease(ownProps.caption))
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Counter);

总结

  • React的设计思想是: UI=render(data), flux架构很好的遵循了单向数据流的原则
  • Flux因为存在多个Store, 导致数据容易Store之间的依赖
  • Redux只有唯一的一个Store, 有效的填补了Flux的缺陷
  • react-redux库提供了全局的Provider, store不需要层层传递, 而且通过connect方法简化了容器组件的创建