Ashley’s Blog

A blogging framework for Ashley.

Redux

Redux 三大原则

  • 单一数据源。 整个应用的 state 被储存在一棵 object tree 中,它只有一个单一的 store 。
  • State 是只读的。 惟一改变 state 的办法就是触发 action,action 是一个描述要发生什么的对象。
  • 纯函数的形式来执行修改。 为了描述 action 如何改变 state tree ,你需要编写 reducers。

同 Flux 的关系

Redux 没有 dispatcher 的概念,它依赖纯函数来替代事件处理器。Redux 设想你永远不会变动你的数据。 细节有所不同:分别体现在 reducer 和 state 上。

Action

Actions 是把数据从应用传到 store 的有效载荷。它是 store 数据的惟一来源。 Actions 表明有事情发生了,包含了更新 state 所需要的足够多的信息。 Action 本质是 JavaScript 普通对象。

1
2
3
4
5
6
7
8
{
      type: 'ADD_TODO',
      text: 'Build my first Redux app'
}
{
    type: 'DELETE_TODO',
    index: 5
}

Action 创建函数

1
2
3
4
5
6
function addTodo(text) {
  return {
     type: ADD_TODO,
     text
   };
}

Reducer

reducer 就是一个函数,接收旧的 state 和 action,返回新的 state。

1
(previousState, action) => newState

不要修改 state。

在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。

1
2
3
4
5
6
7
8
9
10
{
  visibilityFilter: 'SHOW_ALL',
  todos: [{
    text: 'Consider using Redux',
    completed: true,
  }, {
    text: 'Keep all state in a single tree',
    completed: false
  }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  case ADD_TODO:
    return Object.assign({}, state, {
      todos: [...state.todos, {
        text: action.text,
        completed: false
      }]
    });
  case COMPLETE_TODO:
    return Object.assign({}, state, {
      todos: [
        ...state.todos.slice(0, action.index),
        Object.assign({}, state.todos[action.index], {
          completed: true
        }),
        ...state.todos.slice(action.index + 1)
      ]
    });
  default:
    return state;
  }
}

combineReducers

一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  };
}

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;
  }
}

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

最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux';

const todoApp = combineReducers({
  visibilityFilter,
  todos
});

export default todoApp;

Store

Store 就是把action and reducers联系到一起的对象。 Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createStore } from 'redux';
import todoApp from './reducers';

let store = createStore(todoApp);

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(completeTodo(0));
store.dispatch(completeTodo(1));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));

// 停止监听 state 更新
unsubscribe();

数据流

  • 调用 store.dispatch(action)。
  • Redux store 调用传入的 reducer 函数。
  • 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
  • Redux store 保存了根 reducer 返回的完整 state 树。

With React

Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。 Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

1
npm install --save react-redux

只在最顶层组件里使用 Redux。

内部组件应该像木偶一样保持“呆滞”,所有数据都通过 props 传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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={this.props.visibleTodos}
          onTodoClick={index =>
            dispatch(completeTodo(index))
          } />
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            dispatch(setVisibilityFilter(nextFilter))
          } />
      </div>
    );
  }
}

// Which props do we want to inject, given the global state?
function select(state) {
  return {
    visibleTodos: selectTodos(state.todos, state.visibilityFilter),
    visibilityFilter: state.visibilityFilter
  };
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(App);

异步action 及 middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                                                                                  ┌──────────────┐
                    ┌─────────────┐                                           ┌──▶│ subReducer 1 │
               ┌───▶│Middleware 1 │                                           │   └──────────────┘
               │    └─────────────┘                                           │           │       
               │           │                                                  │           ▼       
┌─────────────┐    │           │              ┌───────────────┐    ┌──────────┐   │   ┌──────────────┐
│   action'   │────┘           ▼          ┌──▶│store.dispatch │───▶│ reducer  │───┘   │ subReducer m │
└─────────────┘         ┌─────────────┐   │   └───────────────┘    └──────────┘       └──────────────┘
                    │Middleware n │   │                                                   │       
                    └─────────────┘   │                                                   │       
                           │          │                                                   ▼       
                           │          │                                           ┌──────────────┐
                           └──────────┘                                           │    state     │
                           plain action                                           └──────────────┘

通过使用指定的 middleware,action creator 除了返回 action 对象外还可以返回函数。这时,这个 action creator 就成为了 thunk。

当 action creator 返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { createStore, applyMiddleware } from 'redux';
import { selectReddit, fetchPosts } from './actions';
import rootReducer from './reducers';

const loggerMiddleware = createLogger();

const createStoreWithMiddleware = applyMiddleware(
  thunkMiddleware, // 允许我们 dispatch() 函数
  loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

store.dispatch(selectReddit('reactjs'));
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export function fetchPosts(reddit) {

  // Thunk middleware 知道如何处理函数。
  // 这里把 dispatch 方法通过参数的形式参给函数,
  // 以此来让它自己也能 dispatch action。

  return function (dispatch) {

    // 首次 dispatch:更新应用的 state 来通知
    // API 请求发起了。

    dispatch(requestPosts(reddit));

    // thunk middleware 调用的函数可以有返回值,
    // 它会被当作 dispatch 方法的返回值传递。

    // 这个案例中,我们返回一个等待处理的 promise。
    // 这并不是 redux middleware 所必须的,但是我们的一个约定。

    return fetch(`http://www.reddit.com/r/${reddit}.json`)
      .then(response => response.json())
      .then(json =>

        // 可以多次 dispatch!
        // 这里,使用 API 请求结果来更新应用的 state。

        dispatch(receivePosts(reddit, json))
      );

      // 在实际应用中,还需要
      // 捕获网络请求的异常。
  };
}

redux-thunk 支持异步的 middleware 包装了 store 的 dispatch() 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,如函数。你所使用的任何 middleware 都可以以自己的方式解释你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。

1
2
3
4
5
6
7
8
9
10
function thunkMiddleware(_ref) {
  var dispatch = _ref.dispatch;
  var getState = _ref.getState;

  return function (next) {
    return function (action) {
      return typeof action === 'function' ? action(dispatch, getState) : next(action);
    };
  };
}