深刻理解Redux

前面的话

  Redux是Flux思想的另外一种实现方式。Flux是和React同时面世的。React用来替代jQuery,Flux用来替换Backbone.js等MVC框架。在MVC的世界里,React至关于V(view)的部分,只涉及页面的渲染。一旦涉及应用的数据管理部分,仍是交给Model和Controller。不过,Flux并非一个MVC框架,它用一种新的思路来管理数据。本文将详细介绍Redux的内容javascript

 

MVC

  MVC是业界普遍接受的一种前端应用框架类型,这种框架把应用分为三个部分:css

  Model(模型)负责管理数据,大部分业务逻辑应该放在Model中前端

  View(视图)负责渲染用户页面,应该避免在View中涉及业务逻辑java

  Controller(控制器)负责接受用户输入,根据用户输入调用相应的Model部分逻辑,把产生的数据结果交给View部分,让View渲染出必要的输出react

  MVC框架提出的数据流很理想,用户请求先到达Controller,由Controller调用Model得到数据,而后把数据交给View。可是,在实际框架实现中,老是容许View和Model直接通讯npm

  然而,在MVC中让View和Model直接对话就是灾难redux

 

Flux

  Facebook用Flux框架来替代原有的MVC框架,这种框架包含四个部分:数组

  Dispatcher负责动做分发,维持Store之间的依赖关系缓存

  Store负责存储数据和处理数据相关逻辑服务器

  Action驱动Dispatcher的javascript对象

  View视图负责显示用户界面

  若是非要把Flux和MVC作一个对比。那么,Flux的Dispatcher至关于MVC的Controller,Flux的store至关于MVC的model,Flux的View对应于MVC的View,Action对应给MVC框架的用户请求

  一、Dispatcher

import {Dispatcher} from 'flux';
export default new Dispatcher();

  二、Action

  定义Action一般须要两个文件,一个定义action的类型,一个定义action的构造函数。分红两个文件的缘由是在Store中会根据action类型作不一样操做,也就有单独导入action类型的须要

export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
import * as ActionTypes from './ActionTypes.js';
import AppDispatcher from './AppDispatcher.js';

export const increment = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  });
};

export const decrement = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  });
};

  三、Store

  一个Store也是一个对象,这个对象用来存储应用状态,同时还要接受Dispatcher派发的动做,根据动做来决定是否要更新应用状态

  一个EventEmitter实例对象支持下列相关函数

emit函数:能够广播一个特定事件,第一个参数是字符串类型的事件名称
on函数:能够增长一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数
removeListener函数: 和on函数作的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数,和on函数同样,第一个参数是事件名称,第二个参数是处理函数

  [注意]若是要调用removeListener函数,就必定要保留对处理函数的引用

import AppDispatcher from '../AppDispatcher.js';
import * as ActionTypes from '../ActionTypes.js';
import {EventEmitter} from 'events';

const CHANGE_EVENT = 'changed';

const counterValues = {
  'First': 0,
  'Second': 10,
  'Third': 30
};


const CounterStore = Object.assign({}, EventEmitter.prototype, {
  getCounterValues: function() {
    return counterValues;
  },

  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  }

});

CounterStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    counterValues[action.counterCaption] ++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.counterCaption] --;
    CounterStore.emitChange();
  }
});

export default CounterStore;

  四、View

   存在于Flux框架中的React组件须要实现如下几个功能

  (1)建立时读取Store上状态来初始化组件内部状态

  (2)当Store上状态发生变化时,组件要马上同步更新内部状态保持一致

  (3)View若是要改变Store状态,必须并且只能派发action

// 父组件
class
ControlPanel extends Component { render() { return ( <div style={style}> <Counter caption="First" /> <Counter caption="Second" /> <Counter caption="Third" /> <hr/> <Summary /> </div> ); } } export default ControlPanel;
// 子组件
class Counter extends Component {
  constructor(props) {
    super(props);
    this.onChange = this.onChange.bind(this);
    this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
    this.onClickDecrementButton = this.onClickDecrementButton.bind(this);
    this.state = {count: CounterStore.getCounterValues()[props.caption]}
  }
  shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) || (nextState.count !== this.state.count);
  }
  componentDidMount() {
    CounterStore.addChangeListener(this.onChange);
  }
  componentWillUnmount() {
    CounterStore.removeChangeListener(this.onChange);
  }
  onChange() {
    const newCount = CounterStore.getCounterValues()[this.props.caption];
    this.setState({count: newCount});
  }
  onClickIncrementButton() {
    Actions.increment(this.props.caption);
  }
  onClickDecrementButton() {
    Actions.decrement(this.props.caption);
  }
  render() {
    const {caption} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button>
        <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button>
        <span>{caption} count: {this.state.count}</span>
      </div>
    );
  }
}
Counter.propTypes = {
  caption: PropTypes.string.isRequired
};
export default Counter;

【优点】

  在Flux中,Store只有get方法,没有set方法,根本不可能直接去修改其内部状态,View只能经过get方法获取Store的状态,没法直接去修改状态,若是View想要修改Store的状态,只能派发一个action对象给Dispatcher

【不足】

  一、Store之间依赖关系

  在Flux的体系中,若是两个Store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数

  二、难以进行服务器端渲染

  三、Store混杂了逻辑和状态

 

Redux

  Redux的含义是Reducer+Flux。Reducer是一个计算机科学中的通用概念。以Javascript为例,数组类型有reduce函数,接受的参数是一个reducer,reducer作的事情就是把数组全部元素依次作规约,对每一个元素都调用一次参数reducer,经过reducer函数完成规约全部元素的功能

  Flux的基本原则是单向数据流,Redux在此基础上强调三个基本原则:

  一、惟一数据源

  二、保持状态只读

  三、数据改变只经过纯函数完成

//actionTypes
export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
//actions
import * as ActionTypes from './ActionTypes.js';

export const increment = (counterCaption) => {
  return {
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  };
};

export const decrement = (counterCaption) => {
  return {
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  };
};
//store
import {createStore} from 'redux';
import reducer from './Reducer.js';

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

const store = createStore(reducer, initValues);

export default store;
//reducer
import * as ActionTypes from './ActionTypes.js';

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

  switch (action.type) {
    case ActionTypes.INCREMENT:
      return {...state, [counterCaption]: state[counterCaption] + 1};
    case ActionTypes.DECREMENT:
      return {...state, [counterCaption]: state[counterCaption] - 1};
    default:
      return state
  }
}
// 父组件
class ControlPanel extends Component {
  render() {
    return (
      <div style={style}>
        <Counter caption="First" />
        <Counter caption="Second" />
        <Counter caption="Third" />
        <hr/>
        <Summary />
      </div>
    );
  }
}
// 子组件
class Counter extends Component {
  constructor(props) {
    super(props);
    this.onIncrement = this.onIncrement.bind(this);
    this.onDecrement = this.onDecrement.bind(this);
    this.onChange = this.onChange.bind(this);
    this.getOwnState = this.getOwnState.bind(this);
    this.state = this.getOwnState();
  }
  getOwnState() {
    return {value: store.getState()[this.props.caption]};
  }
  onIncrement() {
    store.dispatch(Actions.increment(this.props.caption));
  }
  onDecrement() {
    store.dispatch(Actions.decrement(this.props.caption));
  }
  onChange() {
    this.setState(this.getOwnState());
  }
  shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) ||
      (nextState.value !== this.state.value);
  }
  componentDidMount() {
    store.subscribe(this.onChange);
  }
  componentWillUnmount() {
    store.unsubscribe(this.onChange);
  }
  render() {
    const value = this.state.value;
    const {caption} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={this.onIncrement}>+</button>
        <button style={buttonStyle} onClick={this.onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}
Counter.propTypes = {
  caption: PropTypes.string.isRequired
};

 

容器和展现

  一个React组件基本上要完成如下两个功能:

  一、读取Store的状态,用于初始化组件的状态,同时还要监听Store的状态改变;当Store状态发生变化时,须要更新组件状态,从而驱动组件从新渲染;当须要更新Store状态时,就要派发action对象

  二、根据当前props和state,渲染出用户界面

  让一个组件只专一作一件事。因而,按照这两个功能拆分红两个组件。这两个组件是父子组件的关系。业界对于这样的拆分有多种叫法,承担第一个任务的组件,也就是负责和redux打交道的组件,处于外层,被称为容器组件;只专业负责渲染界面的组件,处于内层,叫作展现组件

  展现组件,又称为傻瓜组件,就是一个纯函数,根据props产生结果。实际上,让展现组件无状态,只根据props来渲染结果,是拆分的主要目的之一。状态所有交给容器组件去处理

function Counter (props){
    const {caption, onIncrement, onDecrement, value} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={onIncrement}>+</button>
        <button style={buttonStyle} onClick={onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}

  或者,直接使用解构赋值的方法

function Counter ({caption, onIncrement, onDecrement, value} ){
    return (
      <div>
        <button style={buttonStyle} onClick={onIncrement}>+</button>
        <button style={buttonStyle} onClick={onDecrement}>-</button>
        <span>{caption} count: {value}</span>
      </div>
    );
  }
}

 

React-redux

  react-redux遵循将组件分红展现组件和容器组件的规范。react-redux提供了两个功能:

  一、Provider组件,可让容器组件默承认以取得state,而不用当容器组件层级很深时,一级级将state传下去

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';

import ControlPanel from './views/ControlPanel';
import store from './Store.js';

import './index.css';

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

  二、connect方法,用于从展现组件生成容器组件。connect的意思就是将这两种组件链接起来

import React, { PropTypes } from 'react';
import * as Actions from '../Actions.js';
import {connect} from 'react-redux';

const buttonStyle = {
  margin: '10px'
};

function Counter({caption, onIncrement, onDecrement, value}) {
  return (
    <div>
      <button style={buttonStyle} onClick={onIncrement}>+</button>
      <button style={buttonStyle} onClick={onDecrement}>-</button>
      <span>{caption} count: {value}</span>
    </div>
  );
}

Counter.propTypes = {
  caption: PropTypes.string.isRequired,
  onIncrement: PropTypes.func.isRequired,
  onDecrement: PropTypes.func.isRequired,
  value: PropTypes.number.isRequired
};

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

function mapDispatchToProps(dispatch, ownProps) {
  return {
    onIncrement: () => {
      dispatch(Actions.increment(ownProps.caption));
    },
    onDecrement: () => {
      dispatch(Actions.decrement(ownProps.caption));
    }
  }
}

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

  关于mapDispatchToProps函数的简化过程以下

   初始代码以下

const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    fetchUsers: () => dispatch(fetchUsers()),
    fetchCategories: () => dispatch(fetchCategories()),
    fetchPosts: () => dispatch(fetchPosts())
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)

  再次简化以下

const mapDispatchToProps = {
  fetchUsers: () => fetchUsers(),
  fetchCategories: () => fetchCategories(),
  fetchPosts: () => fetchPosts()
}
export default connect(mapStateToProps, mapDispatchToProps)(Home)

  最终优化以下

export default connect(mapStateToProps, { fetchUsers, fetchCategories, fetchPosts })(Home)

 

模块化应用

  从架构出发,开始一个新应用时,有几件事情是必定要考虑清楚的:

  一、代码文件的组织结构

  二、肯定模块的边界

  三、Store的状态树设计

【代码文件的组织结构】

  Redux应用适合于按功能组织,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码。在Redux中,不一样的角色就是reducer、actions和视图。而应用功能对应的就是用户界面上的交互模块

  以Todo应用为例,这个应用的两个基本功能就是TodoList和Filter,因此代码能够这样组织:

todoList/
    actions.js
    actionTypes.js
    index.js
    reduce.js
    views/
        component.js
        container.js
filter/
    actions.js
    actionTypes.js
    index.js
    reduce.js
    views/
        component.js
        container.js

【模块接口】

  不一样功能模块之间的依赖关系应该简单而清晰,也就是所谓的保持模块之间低耦合性;一个模块应该把本身的功能封装得很好,让外界不要太依赖于本身内部的结构,这样不会由于内部的变化而影响外部模块的功能,这就是所谓的高内聚性

【状态树的设计】

  状态树的设计须要遵循以下几个原则:

  一、一个模块控制一个状态节点

  二、避免冗余数据

  三、树形结构扁平

  对于Todo应用的状态树设计以下

{
  todos: [
    {
      text: 'first todo',
      completed: false,
      id: 0
    },
    {
      text: 'second todo',
      completed: true,
      id: 1
    },    
  ],
  // 'all'、'completed'、'uncompleted'
  filter: 'all'
}

 

reselect

  reselect库的原理是只要相关状态没有改变,那就直接使用上一次的缓存结果。reselect用来创造选择器,接收一个state做为参数的函数,返回的数据是某个mapStateToProps须要的结果

  首先,安装reselect库

npm install --save reselect

  reselect提供了创造选择器的createSelector函数,这是一个高阶函数,也就是接受函数为参数来产生一个新函数的函数

  createSelector 接收一个 input-selectors 数组和一个转换函数做为参数。若是 state tree 的改变会引发 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 做为参数,并返回结果。若是 input-selectors 的值和前一次的同样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。

import { createSelector } from 'reselect'

const getVisibilityFilter = (state) => state.visibilityFilter
const getTodos = (state) => state.todos

export const getVisibleTodos = createSelector(
  [ getVisibilityFilter, getTodos ],
  (visibilityFilter, todos) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed)
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed)
    }
  }
)

  在上例中,getVisibilityFilter 和 getTodos 是 input-selector。由于他们并不转换数据,因此被建立成普通的非记忆的 selector 函数。可是,getVisibleTodos 是一个可记忆的 selector。他接收 getVisibilityFilter 和 getTodos 为 input-selector,还有一个转换函数来计算过滤的 todos 列表

  reselect的典型应用以下所示

// selector
export const getCategories = state => {
  return state.category
}
export const getCategoriesSortByNumber = createSelector(getCategories, categories =>
  categories.sort((v1, v2) => {
    return v1.number - v2.number
  })
)
export const getCategoryDatas = createSelector(getCategoriesSortByNumber, categoriesSortByNumber => 
  categoriesSortByNumber.map(t => {
    return $_setChildren(categoriesSortByNumber, t)
  }).map(t => {
    return Object.assign(t, {
      index: $_getIndex(t.number),
      des: t.children.length ? t.children.length : '',
      title: t.name,
      key: t.number,
      className: 'styled-categorylist',
      url: t.children.length ? `/category/${t.number}` : '',
      parentUrl: `/category/${$_getParentNumber(t)}`,
      nextChildNumber: $_getFirstChildNumber(t)
    })
  })  
)
export const getCategoryDatasByNumber = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.reduce((obj, t) => {
    obj[t.number] = t
    return obj
  }, {})
)
export const getCategoryRootDatas = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.filter(t => {
    return Number(String(t.number).slice(2)) === 0
  })
)
export const getCategoryDatasById = createSelector(getCategoryDatas, categoryDatas =>
  categoryDatas.reduce((obj, t) => {
    obj[t._id] = t
    return obj
  }, {})
)

 

常见错误

  在使用redux的过程当中,会出现以下的常见错误

【错误:reducers不能触发actions】

Uncaught Error: Reducers may not dispatch actions.

  通常来讲,出现"Reducedrs may not dispatch actions"的错误,是由于reducer中出现路由跳转语句,而跳转到的语句正好发送了dispatch。从而,reducer再也不是纯函数

  错误代码以下所示:

export const logIn = admin => ({type: LOGIN, admin})

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      // 将用户信息保存到sessionStorage中
      sessionStorage.setItem('token', token)
      sessionStorage.setItem('user', JSON.stringify(user))
      // 跳转到首页
      history.push('/')
      return { token, user }
...

  有两种解决办法

  一、给路由跳转语句设置延迟定时器,从而避免在当前reducer尚未返回值的状况下,又发送新的dispatch

export const logIn = admin => ({type: LOGIN, admin})

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      // 将用户信息保存到sessionStorage中
      sessionStorage.setItem('token', token)
      sessionStorage.setItem('user', JSON.stringify(user))
      // 跳转到首页
      setTimeout(() => {
        history.push('/')
      },0)
      return { token, user }
...

  二、将reducer中的逻辑放到dispatch中

export const logIn = (admin) => {
  let { token, user } = admin
  // 将用户信息保存到sessionStorage中
  sessionStorage.setItem('token', token)
  sessionStorage.setItem('user', JSON.stringify(user)) 
  // 跳转到首页
  history.push('/')
  return {type: LOGIN, admin}
}

// reducer
const login = (state = initialState, action) => {
  switch (action.type) {
    case LOGIN:
      let { token, user } = action.admin
      return { token, user }
...

【action函数中没法执行return后的语句】

   例如,在下面代码中,控制台只能输入'111',而不能输出'222'

export const updatePost = payload => {
  console.log('111')
  return dispatch => {
    console.log('222')
    fetchModule({
      dispatch,
      url: `${BASE_POST_URL}/${payload._id}`,
      method: 'put',
      data: payload,
      headers: { Authorization: sessionStorage.getItem('token') },
      success(result) {
        console.log(result)
        dispatch({ type: UPDATE_POST, doc: result.doc })
      }
    })
  }
}

  出现这个问题的缘由很是简单,是由于没有使用this.props.updatePost,而直接使用了updatePost方法致使的

  加入以下语句既可解决

let { updatePost } = this.props

【redux中的state发生变化,但页面没有从新渲染】

  通常地,是由于展开运算符使用不当所至

  而对于对象的展开运算符,则须要把...state放到第一个条目位置,由于后面的条目会覆盖展开的部分

return {...item, completed: !item.completed}

【reducer中不能使用undefined】

  一、reducer中state不能返回undefined,能够用null代替

// reducer
const filter = (state = null, action) => {
  switch (action.type) {
    case SHOW_FILTER:
      return action.filter
    default:
      return state
  }
}

   二、一样地,action.filter表示空值,不能为undefined,用null代替

export const setFilter = filter => ({type: SHOW_FILTER, filter})
this
.props.setFilter(null)
相关文章
相关标签/搜索