重要提示: 本教程配套示例代码请前往redux-complete-sample下载,课程中会有大量的示例操作,操作说明均基于这个配套的示例代码仓库,所以为了方便学习,请务必下载安装并启动。

在开始往下阅读之前,我默认你已经学习了前面的课程,并且掌握了Webpack、ES6、React等知识的应用。

在前面的课程,我们已经使用React创建了一个应用,但是在实际项目中,面对复杂业务逻辑的挑战,如何清晰高效的管理应用内的数据流动成为了关键。

Flux思想已经在提出后得到逐步推广,并广泛应用到实际的项目中。facebook的flux实现,开源社区的reflux、redux等类库开始涌现并得到了广大开发者的认同和使用。

Redux以其简单易用、文档齐全易懂等优点在开源社区得到开发者的一致好评,所以接下来让我们一起走进Redux,学习并将其使用到我们实际的项目开发中。

1. 基本介绍

React 已经帮我们在视图层解决了禁止异步和直接操作 DOM 等问题,让页面的高效渲染和组件化开发成为了可能。美中不足的是,React 依旧把处理 state 中数据的问题留给了你,那么,Redux的出现就是为了帮你解决这个问题。

1.1 Flux & Redux

最初,Facebook官方提出了FLUX思想管理数据流,同时也给出了自己的实现方案flux来管理React应用。

看图说话:
1.在view中触发action中的方法后
2.action发送dispatch
3.store接收新的数据进行合并,然后触发View中绑定在store上的方法
4.最后通过修改局部state来改变view的展示

看图说话:
1.view直接触发dispatch
2.dispatch将action发送到reducer中后,根节点上会更新props,改变全局view。
3.redux将view和store的绑定从手动编码中提取出来,放在了统一规范放在了自己的体系中。

相对于Flux而言,Redux的实现更简单,思路更清晰,写的代码也相对更少;只维护单一的 store;在github上收货了16000+的star,广受欢迎...

1.2 对 Redux 的介绍

  • Redux 是 State 容器,提供可预测化的状态管理
  • 它可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试
  • 还提供 redux-devtools 让开发者享受超爽的开发体验
  • 体小精悍(只有2kB)且没有任何依赖
  • 拥有丰富的生态圈:教程、开发者工具、路由、组件、中间件、工具集...

2. 快速上手

本次教程内容的所有示例可以在

$ git clone git@github.com:GuoYongfeng/redux-complete-sample.git
$ cd redux-complete-sample && npm install
$ cd demo-redux-start
$ webpack-dev-server --progress --colors

示例代码快速体验

import { createStore } from 'redux';

// 这是一个 reducer,形式为 (state, action) => state 的纯函数。描述了 action 如何把 state 转变成下一个 state。

// state 的形式取决于你,可以是基本类型、数组、对象、
// 甚至是 Immutable.js 生成的数据结构。惟一的要点是
// 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}

// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 一个单纯渲染页面内容的函数
const PureRender = () => {
  document.body.innerText = store.getState();
}

// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(PureRender);
// 执行渲染函数
PureRender();

// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
document.addEventListener('click', function( e ){
  // store dispatch 调度分发一个 action(fire)
  store.dispatch({ type: 'DECREMENT'});
})

3. 理解 Redux 的核心概念

3.1 Action & Action Creator

在 Redux 中,改变 State 只能通过 action,它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。。并且,每一个 action 都必须是 Javascript 的简单对象,例如:

{
  type: 'ADD_TODO',
  text: 'Learn Redux'
}

Redux 要求 action 是可以被序列化的,使这得应用程序的状态保存、回放、Undo 之类的功能可以被实现。因此,action 中不能包含诸如函数调用这样的不可序列化字段。

action 的格式是有建议规范的,可以包含以下字段:

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'
  },
  `meta: {}`
}

如果 action 用来表示出错的情况,则可能为:

{
  type: 'ADD_TODO',
  payload: new Error(),
  error: true
}

type 是必须要有的属性,其他都是可选的。完整建议请参考 Flux Standard Action(FSA) 定义。已经有不少第三方模块是基于 FSA 的约定来开发了。

Action Creator

事实上,创建 action 对象很少用这种每次直接声明对象的方式,更多地是通过一个创建函数。这个函数被称为Action Creator,例如:

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  };
}

Action Creator 看起来很简单,但是如果结合上 Middleware 就可以变得非常灵活,后面会专门讲 Middleware 。

3.2 Reducer

我们先来看一下 Javascript 中 Array.prototype.reduce 的用法:

const initState = '';
const actions = ['a', 'b', 'c'];
// 传入当前的 state 和 action ,返回新的 state
const newState = actions.reduce((curState, action) => curState + action);
console.log( newState );

对应的理解,Redux 中的 reducer 是一个纯函数,传入state和action,返回一个新的state tree,简单而纯粹的完成某一件具体的事情,没有依赖,简单而纯粹是它的标签

const counter = (state = 0, action) => {
  switch (action.type) {
      case 'INCREMENT':
        return state + 1;
      case 'DECREMENT':
        return state - 1;
      default:
        return state;
  }
}

3.3 Store

Store 就是用来维持应用所有的 state 树 的一个对象。 改变 store 内 state 的惟一途径是对它 dispatch 一个 action

Store 是一个具有以下四个方法的对象:

  • getState()
  • dispatch(action)
  • subscribe(listener)
  • replaceReducer(nextReducer)

3.3.1 getState()

返回应用当前的 state 树。 它与 store 的最后一个 reducer 返回值相同。

返回值:应用当前的 state 树。

3.3.2 dispatch(action)

dispatch 分发 action。这是触发 state 变化的惟一途径。

会使用当前 getState() 的结果和传入的 action 以同步方式的调用 store 的 reduce 函数。返回值会被作为下一个 state。从现在开始,这就成为了 getState() 的返回值,同时变化监听器(change listener)会被触发。

在 Redux 里,只会在根 reducer 返回新 state 结束后再会调用事件监听器,因此,你可以在事件监听器里再做 dispatch。惟一使你不能在 reducer 中途 dispatch 的原因是要确保 reducer 没有副作用。如果 action 处理会产生副作用,正确的做法是使用异步 action 创建函数。

示例:

import { createStore } from 'redux'

// reducer
const todos = (state = [''], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      console.log(Object.assign([], state, [action.text]))
      return Object.assign([], state, [action.text]);
    default:
      return state;
  }
}

let store = createStore(todos, [ 'Use Redux' ])

// action creator
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

// dispatch
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))

3.3.3 subscribe(listener)

添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。这是一个底层 API。多数情况下,你不会直接使用它,会使用一些 React(或其它库)的绑定。

示例

import { createStore } from 'redux'

// reducer
const todos = (state = [''], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return Object.assign([], state, [action.text]);
    default:
      return state;
  }
}

let store = createStore(todos, [ 'Use Redux' ])

// action creator
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

const handleChange =  () => {
  console.log(store.getState());
}

let unsubscribe = store.subscribe(handleChange)

handleChange()

// dispatch
store.dispatch(addTodo('Read the docs'))
store.dispatch(addTodo('Read about the middleware'))

4. Redux 的顶层 API 介绍

4.1 createStore

调用方式:createStore(reducer, [initialState])

创建一个 Redux store 来以存放应用中所有的 state,应用中应有且仅有一个 store。 这个API返回一个store,这个store中保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。我们来看一个示例

我们可以试着模拟 createStore,深入了解其原理

// reducer 纯函数,具体的action执行逻辑
const counter = (state = 0, action) => {
  switch (action.type) {
      case 'INCREMENT':
        return state + 1;
      case 'DECREMENT':
        return state - 1;
      default:
        return state;
  }
}

// 模拟create store,了解其原理
const createStore = (reducer) => {
  let state;
  let listeners = [];

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  }

  const subscribe = (listener) => {
      listeners.push(listener);
      return () => {
        listeners = listeners.filter(item => item !== listener);
      }
  }

  dispatch({});

  return { getState, dispatch, subscribe };
}

const store = createStore(counter);

// view 对应到React里面的component
const PureRender = () => {
  document.body.innerText = store.getState();
}

// store subscribe 订阅或是监听view(on)
store.subscribe(PureRender);
PureRender();

document.addEventListener('click', function( e ){
  // store dispatch 调度分发一个 action(fire)
  store.dispatch({ type: 'DECREMENT'});
})

4.2 combineReducers

调用方式:combineReducers(reducers)

随着应用变得复杂,需要对 reducer 函数进行拆分,拆分后的每一块独立负责管理 state 的一部分。把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore。

示例如下

代码清单:reducer/todos.js

export default function todos(state = [], action) {
  switch (action.type) {
  case 'ADD_TODO':
    return state.concat([action.text])
  default:
    return state
  }
}

代码清单:reducer/counter.js

export default function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

代码清单:reducers/index.js

import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'

export default combineReducers({
  todos,
  counter
})

代码清单:App.js

import { createStore } from 'redux'
import reducer from './reducer/index.js'

let store = createStore(reducer)
console.log('当前的 state :', store.getState())

store.dispatch({
  type: 'ADD_TODO',
  text: 'Use Redux'
})
store.dispatch({
  type: 'INCREMENT',
})
console.log('改变后的 state :', store.getState())

4.3 applyMiddleware

调用方式:applyMiddleware(...middlewares)

使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。

具体用法我们高级部分详细说明。

4.4 bindActionCreators

调用方式:bindActionCreators(actionCreators, dispatch)

惟一使用 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 Redux store 或 dispatch 传给它。

具体用法我们高级部分详细说明。

4.5 compose

调用方式:compose(...functions)

compose 用来实现从右到左来组合传入的多个函数,它做的只是让你不使用深度右括号的情况下来写深度嵌套的函数,仅此而已。

5. 使用 React-redux 连接 react 和 redux

5.1 没有react-redux的写法

封装一个组件,将组件和Redux做基本的组合

import { createStore } from 'redux';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

// reducer 纯函数,具体的action执行逻辑
const counter = (state = 0, action) => {
  switch (action.type) {
      case 'INCREMENT':
        return state + 1;
      case 'DECREMENT':
        return state - 1;
      default:
        return state;
  }
}

const store = createStore(counter);

// Counter 组件
class Counter extends Component {
  render(){
    return (
      <div>
        <h1>{this.props.value}</h1>
        <button onClick={this.props.onIncrement}>点击加1</button>
        <button onClick={this.props.onDecrement}>点击减1</button>
      </div>
    )
  }
}

const PureRender = () => {
  ReactDOM.render(
      <Counter
        value={store.getState()}
        onIncrement={ () => store.dispatch({type: "INCREMENT"}) }
        onDecrement={ () => store.dispatch({type: "DECREMENT"}) }
      />, document.getElementById('app')
  );
}

// store subscribe 订阅或是监听view(on)
store.subscribe(PureRender)
PureRender()

5.2 React-redux 提供的 connect 和 Provider

<Provider store> 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在`<Provider> 中才能使用 connect() 方法。

ReactDOM.render(
  {/*  使组件层级中的 connect() 方法都能够获得 Redux store */}
  <Provider store={store}>
    {/* 这里传入的组件MyRootComponent是组件层级的根组件 */}
    <MyRootComponent />
  </Provider>,
  rootEl
);

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) connect方法是用来连接 React 组件与 Redux store,连接操作不会改变原来的组件类,反而返回一个新的已与 Redux store 连接的组件类。

使用react-redux的一个简单完整示例

import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider, connect } from 'react-redux'

// 这是一个展示型组件 Counter
class Counter extends Component {
  render() {
    const { value, onIncreaseClick } = this.props
    return (
      <div>
        <span>{value}</span>
        <button onClick={onIncreaseClick}>戳我加1</button>
      </div>
    )
  }
}

Counter.propTypes = {
  value: PropTypes.number.isRequired,
  onIncreaseClick: PropTypes.func.isRequired
}

// Action
const increaseAction = { type: 'increase' }

// Reducer
function counter(state = { count: 0 }, action) {
  let count = state.count
  switch (action.type) {
    case 'increase':
      return { count: count + 1 }
    default:
      return state
  }
}

// Store
let store = createStore(counter)

// Map Redux state to component props
function mapStateToProps(state) {
  // 这里拿到的state就是store里面给的state
  console.log(state);
  return {
    value: state.count
  }
}

// Map Redux actions to component props
function mapDispatchToProps(dispatch) {
  // dispatch(action) { }
  return {
    onIncreaseClick: () => dispatch(increaseAction)
  }
}

class App extends Component {
  render() {
    // store里的state经过connect连接后给了根组件的props
    console.log(this.props);
    return (
      <div>
        <h1>学习使用react-redux</h1>
        <Counter {...this.props} />
      </div>
    )
  }
}

// Connected Component
let RootApp = connect(
  mapStateToProps,
  mapDispatchToProps
)(App)

ReactDOM.render(
  <Provider store={store}>
    <RootApp />
  </Provider>,
  document.getElementById('app')
)

实际应用中,connect这个部分会比较复杂,我们后续高级部分内容进行补充。

6. 一步步开发一个 TODO 应用

6.1 入口文件

index.js

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'

let store = createStore(todoApp)

let rootElement = document.getElementById('app')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

6.2 Action 创建函数和常量

actions.js

/*
 * action 类型
 */

export const ADD_TODO = 'ADD_TODO';
export const COMPLETE_TODO = 'COMPLETE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * 其它的常量
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
};

/*
 * action 创建函数
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function completeTodo(index) {
  return { type: COMPLETE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

6.3 Reducers

reducers.js

import { combineReducers } from 'redux'
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    default:
      return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case COMPLETE_TODO:
      return [
        ...state.slice(0, action.index),
        Object.assign({}, state[action.index], {
          completed: true
        }),
        ...state.slice(action.index + 1)
      ]
    default:
      return state
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

6.4 容器组件

containers/App.js

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'

class App extends Component {
  render() {
    // Injected by connect() call:
    const { dispatch, visibleTodos, visibilityFilter } = this.props
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            dispatch(addTodo(text))
          } />
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    )
  }
}

App.propTypes = {
  visibleTodos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired,
  visibilityFilter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
}

function selectTodos(todos, filter) {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(todo => todo.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(todo => !todo.completed)
  }
}

// Which props do we want to inject, given the global state?
// Note: use https://github.com/faassen/reselect for better performance.
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  }
}

// 包装 component ,注入 dispatch 和 state 到其默认的 connect(select)(App) 中;
export default connect(select)(App)

6.5 展示组件

components/AddTodo.js

import React, { Component, PropTypes } from 'react'

export default class AddTodo extends Component {
  render() {
    return (
      <div>
        <input type='text' ref='input' />
        <button onClick={(e) => this.handleClick(e)}>
          Add
        </button>
      </div>
    )
  }

  handleClick(e) {
    const node = this.refs.input
    const text = node.value.trim()
    this.props.onAddClick(text)
    node.value = ''
  }
}

AddTodo.propTypes = {
  onAddClick: PropTypes.func.isRequired
}

components/Footer.js

import React, { Component, PropTypes } from 'react'

export default class Footer extends Component {
  renderFilter(filter, name) {
    if (filter === this.props.filter) {
      return name
    }

    return (
      <a href='#' onClick={e => {
        e.preventDefault()
        this.props.onFilterChange(filter)
      }}>
        {name}
      </a>
    )
  }

  render() {
    return (
      <p>
        Show:
        {' '}
        {this.renderFilter('SHOW_ALL', 'All')}
        {', '}
        {this.renderFilter('SHOW_COMPLETED', 'Completed')}
        {', '}
        {this.renderFilter('SHOW_ACTIVE', 'Active')}
        .
      </p>
    )
  }
}

Footer.propTypes = {
  onFilterChange: PropTypes.func.isRequired,
  filter: PropTypes.oneOf([
    'SHOW_ALL',
    'SHOW_COMPLETED',
    'SHOW_ACTIVE'
  ]).isRequired
}

components/Todo.js

import React, { Component, PropTypes } from 'react'

export default class Todo extends Component {
  render() {
    return (
      <li
        onClick={this.props.onClick}
        style={{
          textDecoration: this.props.completed ? 'line-through' : 'none',
          cursor: this.props.completed ? 'default' : 'pointer'
        }}>
        {this.props.text}
      </li>
    )
  }
}

Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired
}

components/TodoList.js

import React, { Component, PropTypes } from 'react'
import Todo from './Todo'

export default class TodoList extends Component {
  render() {
    return (
      <ul>
        {this.props.todos.map((todo, index) =>
          <Todo {...todo}
                key={index}
                onClick={() => this.props.onTodoClick(index)} />
        )}
      </ul>
    )
  }
}

TodoList.propTypes = {
  onTodoClick: PropTypes.func.isRequired,
  todos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired
}

完结寄语

Redux的基础部分的内容就到这里了,我们简单回顾下:基本的认识了redux的思想后,我们通过一个简单的例子快速的体验了redux的使用,通过对action、reducer、store等核心概念的讲解后我们可以理解redux的工作原理;在了解了redux的几个顶层API之后,我们学习了react-redux,并完成了一个基本示例,通过这个示例认识到react和redux结合使用的姿势;最后通过一步步开发一个todo应用来总结我们基础部分的内容学习。

另外,更多深入内容在让Redux来管理你的应用(二)和大家相见。

参考资料