Introduction

首先,为什么会有这个教程呢? 在我尝试学习 Redux 的时候,我发现之前阅读过的一些文章加上个人的经验,让我对 flux 产生了一些误解。当然,我不是说那些关于 flux 的文章写得不好,只是我没能正确地领会其中的概念。到头来,我只是对着各种 flux 框架(Reflux、Flummox、FB Flux)的文档照猫画虎,并试着把它们和之前了解到的理论概念联系起来 (actions / actions creators、 store、 dispatcher)。

等我用了 Redux 之后,我才发现原来 flux 比我想象的要简单很多。这些都归功于 Redux 通过优良的设计减少了样板代码,而其它框架则是为了减少样板代码却又引入了很多新的代码。我现在觉得用通过 Redux 来学习 flux 比通过其他框架高效得多。这就是为什么我想分享给大家这个教程,通过关注 Redux 的用法来理解 flux 的概念。

你可能已经看过这张著名的 flux 的单向数据流图了。

                 _________               ____________               ___________
                |         |             |            |             |           |
                | Action  |------------▶| Dispatcher |------------▶| callbacks |
                |_________|             |____________|             |___________|
                     ▲                                                   |
                     |                                                   |
                     |                                                   |
 _________       ____|_____                                          ____▼____
|         |◀----|  Action  |                                        |         |
| Web API |      | Creators |                                        |  Store  |
|_________|----▶|__________|                                        |_________|
                     ▲                                                   |
                     |                                                    |
                 ____|________           ____________                 ____▼____
                |   User       |         |   React   |               | Change  |
                | interactions |◀--------|   Views   |◀-------------| events  |
                |______________|         |___________|               |_________|

在这个教程里,我们会一步步地向你介绍上图里的各个概念。 我们会把这些概念分成单独的章节来介绍它们存在的意义和作用。 在最后,当我们理解了每一个概念后,我们会发现这张图真是意义深远啊!

在我们开始之前,我们先聊下一 flux 存在的意义以及我们为什么需要它。

假设我们正在构建一个网站应用,那么这个网站应用会由什么组成呢?

  1. 模板/HTML = View
  2. 填充视图的数据 = Model
  3. 获取数据、将所有视图组装在一起、响应用户事件、数据操作等等的逻辑 = Controller

这是我们熟知的非常典型的 MVC,但它和 flux 的概念其实是很像的, 只是在某些表述上有些小小的不同:

  • Model 看起来像 Store
  • 用户事件、数据操作以及它们的处理程序看起来像action creators ->action ->dispatcher -> callback
  • View 看起来像 React view (或者其它类似的概念)

所以,flux 就只是一个新名词么?不全是,但是新名词是很重要的,因为通过引入这些新术语我们可以更准确地表述各种专业术语。

举一个例子,获取数据是一个 action,一个点击是一个 action,一个 input 变化也是一个 action 等等。我们都已经习惯了从我们的应用里分发 action,只是以不同的方式称呼它们。 不同于直接修改 Model 和 View,Flux 确保所有 action 首先通过一个 dispatcher,然后再是 store,最后通知所有的 store 监听器。

为了弄清楚 MVC 和 flux 的不同,我们举一个典型的 MVC 应用的用例:

一个典型的 MVC 应用的流程大致上是这样的:

  1. 用户点击按钮 A
  2. 点击按钮 A 的处理程序触发 Model A 的改变
  3. Model A 的改变处理程序触发 Model B 的改变
  4. Model B 的改变处理程序触发 View B 的改变并重新渲染自身

在这样的一个环境里,当应用出错的时候快速地定位 bug 来源是一件非常困难的事情。 这是因为每个 View 可以监视任何的 Model,并且每个 Model 可以监视其它所有 Model,所以数据会从四面八方涌来,并且被许多源(view 或者 model)改变。

当我们用 flux 以及它的单向数据流的时候,上面的例子就会变成这样子:

  1. 用户点击按钮 A
  2. 点击按钮A的处理程序会触发一个被分发的 action,并改变 Store A
  3. 因为其它的 Store 也被这个 action 通知了,所以 Store B 也会对相同的 action 做出反应
  4. View B 因为 Store A 和 Store B 的改变而收到通知,并重新渲染

来看一下我们是如何避免 Store A 和 Store B 直接相关联的。 Store 只能被 action 修改,别无他选。并且当所有 Store 响应了 action 后,View 才会最终更新。由此可见,数据总是沿着一个方向进行流动:

 action -> store -> view -> action -> store -> view -> action -> ...

上面我们首先从 action 开始我们的用例,下面让我们同样以 action 和 action creator 来开始我们的教程。

Simple action creator

我们在前言中已经简单提到过 action,但具体什么是 action creator,它们又是如何关联到 action 的呢?

其实,通过几行简单的代码就可以解释清楚了!

// action creator 就是函数而已...
var actionCreator = function() {
    // ...负责构建一个 action (是的,action creator 这个名字已经很明显了)并返回它
    return {
        type: 'AN_ACTION'
    }
}

这就完了?是的,仅此而已。

然而,有一件事情需要注意,那就是 action 的格式。flux 一般约定 action 是一个拥有 type 属性的对象。然后按 type 决定如何处理 action。当然,action 依旧可以拥有其他属性,你可以任意存放想要的数据。在后面的章节中,我们会发现 action creator 实际上可以返回 action 以外的其他东西,比如一个函数。这在处理异步时很有用(更多的内容可以查阅 dispatch-async-action.js)。

我们可以直接调用 action creator,如同预期的一样,我们会得到一个 action:

console.log(actionCreator())
// 输出: { type: 'AN_ACTION' }

好了,以上代码没有任何问题,却也毫无用处…在实际的场景中,我们需要的是将 action 发送到某个地方,让关心它的人知道发生了什么,并且做出相应的处理。我们将这个过程称之为分发 action(Dispatching an action)

为了分发 action,我们需要…一个分发函数(= ̄ω ̄=)。 并且,为了让任何对它感兴趣的人都能感知到 action 发起,我们还需要一个注册“处理器(handlers)”的机制。这些 action 的“处理器”在传统的 flux 应用中被称为 store,在下个章节中,我们会介绍它们在 Redux 中叫什么。

至止,我们的应用中包含了以下流程: ActionCreator -> Action

可以在以下链接中了解更多关于 action 和 action creator 的内容:

About state and meet redux

在实际应用中,我们不仅需要 action 告诉我们发生了什么,还要告诉我们需要随之更新数据。这就让我们的应用变的棘手:

  • 如何在应用程序的整个生命周期内维持所有数据?
  • 如何修改这些数据?
  • 如何把数据变更传播到整个应用程序?

于是 Redux 登场。 Redux 是一个“可预测化状态的 JavaScript 容器”。

我们先回顾上述提出的问题并用 Redux 的词汇表给出以下解答(部分词汇也来源于 Flux):

如何在应用程序的整个生命周期内维持所有数据? 以你想要的方式维持这些数据,例如 JavaScript 对象、数组、不可变数据,等等。 我们把应用程序的数据称为状态。这是有道理的,因为我们所说的数据会随着时间的推移发生变化,这其实就是应用的状态。但是我们把这些状态信息转交给了 Redux(还记得么?Redux 就是一个“容纳状态的容器”)。 如何修改这些数据? 我们使用 reducer 函数修改数据(在传统的 Flux 中我们称之为 store)。 Reducer 函数是 action 的订阅者。Reducer 函数只是一个纯函数,它接收应用程序的当前状态以及发生的 action,然后返回修改后的新状态(或者有人称之为归并后的状态)。 如何把数据变更传播到整个应用程序? 使用订阅者来监听状态的变更情况。

Redux 帮你把这些连接起来。总之 Redux 提供了:

  1. 存放应用程序状态的容器
  2. 一种把 action 分发到状态修改器的机制,也就是 reducer 函数
  3. 监听状态变化的机制

我们把 Redux 实例称为 store 并用以下方式创建:

import { createStore } from 'redux'
var store = createStore()

但是当你运行上述代码,你会发现以下异常消息:

Error: Invariant Violation: Expected the reducer to be a function.

这是因为 createStore 函数必须接收一个能够修改应用状态的函数。 我们再试一下:

import { createStore } from 'redux'
var store = createStore(() => {})

看上去没有问题了…

Simple reducer

现在,我们知道如何去创建一个 Redux 实例,并让它管理应用中的 state,下面讲一下这些 reducer 函数是如何转换 state 的。

Reducer 与 Store 区别: 你可能已经注意到,在简介章节中的 Flux 图表中,有 Store,但没有Redux 中的 Reducer。那么,Store 与 Reducer 到底有哪些区别呢? 实际的区别比你想象的简单:Store 可以保存你的 data,而 Reducer 不能。因此在传统的 Flux 中,Store 本身可以保存 state,但在 Redux 中,每次调用 reducer时,都会传入待更新的 state。这样的话,Redux 的 store 就变成了“无状态的 store” 并且改了个名字叫 Reducer。

如上所述,在创建一个 Redux 实例前,需要给它一个 reducer 函数:

import { createStore } from 'redux'
var store_0 = createStore(() => {})

所以每当一个 action 发生时,Redux 都能调用这个函数。往 createStore 传 Reducer 的过程就是给 Redux 绑定 action 处理函数(也就是 Reducer)的过程。action 处理函数在 01_simple-action-creator.js 章节中有讨论过。

// 在 Reducer 中打印一些 log
var reducer = function (...args) {
    console.log('Reducer was called with args', args)
}
var store_1 = createStore(reducer)

// 输出:Reducer was called with args [ undefined, { type: '@@redux/INIT' } ]

看出来了吗?我们的 reducer 被调用了,但我们并没有 dispatch 任何 action,这是因为在初始化应用 state 的时候,Redux dispatch 了一个初始化的 action ({ type: ‘@@redux/INIT’ })在被调用时,一个 reducer 会得到这些参数:(state, action)在应用初始化时,state 还没被初始化,因此它的值是 “undefined”, 这是非常符合逻辑的。

在处理 “init” action 之后,我们应用中的 state 又会是怎么样的呢?

下一章节:Get state

Get state

如何从 Redux 实例中读取 state ?

import { createStore } from 'redux'
var reducer_0 = function (state, action) {
    console.log('reducer_0 was called with state', state, 'and action', action)
}
var store_0 = createStore(reducer_0)
// 输出: reducer_0 was called with state undefined and action { type: '@@redux/INIT' }

// 为了读取 Redux 保存的 state,你可以调用 getState
console.log('store_0 state after initialization:', store_0.getState())
// 输出: store_0 state after initialization: undefined

都已经初始化过了,难道程序的 state 还是 undefined 的?没错,正是如此,到目前为止,我们的 reducer 还什么事都没做过…… 你是否还有印象,我们在 “about-state-and-meet-redux” 那一章里是怎么描述一个 reducer 的预期行为的?

一个 reducer 只是一个函数,它能收到程序当前的 state 与 action, 然后返回一个 modify(又或者学别人一样称之为 reduce )过的新 state 。

我们的 reducer 目前什么都不返回,所以程序的 state 当然只能是 reducer() 返回的那个叫 “undefined” 的东西。

接下来,我们试着在 reducer 收到 undefined 的 state 时,给程序发一个初始状态:

var reducer_1 = function (state, action) {
    console.log('reducer_1 was called with state', state, 'and action', action)
    if (typeof state === 'undefined') {
        return {}
    }

    return state;
}
var store_1 = createStore(reducer_1)
// 输出:reducer_1 was called with state undefined and action { type: '@@redux/INIT' }

console.log('store_1 state after initialization:', store_1.getState())
// 输出:store_1 state after initialization: {}

如我们所愿,现在 Redux 初始化以后返回的 state 变成 {} 了, 感谢ES6,这个模式现在实现起来很清晰:

var reducer_2 = function (state = {}, action) {
    console.log('reducer_2 was called with state', state, 'and action', action)

    return state;
}

var store_2 = createStore(reducer_2)
// 输出: reducer_2 was called with state {} and action { type: '@@redux/INIT' }

console.log('store_2 state after initialization:', store_2.getState())
// 输出: store_2 state after initialization: {}

估计你已经发现了,我们给 reducer_2 的 state 参数传了默认值之后,reducer 就不会再取到 undefined 的 state 了。

小结一下:调用 reducer ,只是为了响应一个派发来的 action 。

接下来,我们在 response 里模拟一个 state 修改,其响应的 action 类型是 ‘SAY_SOMETIHG’

var reducer_3 = function (state = {}, action) {
    console.log('reducer_3 was called with state', state, 'and action', action)

    switch (action.type) {
        case 'SAY_SOMETHING':
            return {
                ...state,
                message: action.value
            }
        default:
            return state;
    }
}

var store_3 = createStore(reducer_3)
// 输出: reducer_3 was called with state {} and action { type: '@@redux/INIT' }

console.log('store_3 state after initialization:', store_3.getState())
// 输出: store_3 state after initialization: {}

到目前为止,我们都还没有得到一个新 state, 因为我们还没有真的派发过任何 action 。 不过在最后一个例子里,有几个点值得注意:

  1. 我假设了 action 里一定包含了一个 type 跟一个 value 。type 基本上是 flux action 已经约定俗成的, 而 value 属性可以是任何类型的。
  2. 这里有个常见模式:在 reducer 里用 switch 来响应对应的 action 。
  3. 用 switch 的时候, 永远 不要忘记放个 “default” 来返回 “state”,否则, 你的 reducer 可能会返回 “undefined” (等于你的 state 就丢了)
  4. 注意 { message: action.value } 是怎么被合并到当前 state 来形成新 state 的, 这全要感谢牛逼的 ES7 notation (Object Spread): { …state, message: action.value }
  5. 还要注意:之所以这个例子能用ES7 Object Spread notation ,是因为它只对 state 里的 { message: action.value} 做了浅拷贝(也就是说, state 第一个层级的属性直接被 { message: action.value } 覆盖掉了 —— 与之相对,其实也有优雅的合并方式 )但是如果数据结构更复杂或者是嵌套的,那处理state更新的时候,很可能还需要考虑一些完全不同的做法:
    • 可以考虑:Immutable.js
    • 可以考虑:Object.assign
    • 可以考虑: 手工合并
    • 又或者考虑用其它任何能满足需要且适合 state 结构的方法,Redux 对此是全无预设的方式的(要记得 Redux 只是个状态的容器)。

现在开始,我们要在 reducer 里处理 action 了,我们将会有多个 reducer 并会组合它们。

前往下一个章节: 05_combine-reducers.js

Combine reducers

我们现在来看一下什么是 reducer

var reducer_0 = function (state = {}, action) {
    console.log('reducer_0 was called with state', state, 'and action', action)

    switch (action.type) {
        case 'SAY_SOMETHING':
            return {
                ...state,
                message: action.value
            }
        default:
            return state;
    }
}

在继续之前,我们先来想象一下拥有很多 action 的 reducer 长什么样子

var reducer_1 = function (state = {}, action) {
    console.log('reducer_1 was called with state', state, 'and action', action)

    switch (action.type) {
        case 'SAY_SOMETHING':
            return {
                ...state,
                message: action.value
            }
        case 'DO_SOMETHING':
            // ...
        case 'LEARN_SOMETHING':
            // ...
        case 'HEAR_SOMETHING':
            // ...
        case 'GO_SOMEWHERE':
            // ...
        // etc.
        default:
            return state;
    }
}

很显然,只有一个 reducer 是 hold 不住我们整个应用中所有 action 操作的(好吧,事实上它能 hold 得住,但这会变得很难维护。)

幸运的是,Redux 不关心我们到底是只有一个 reducer ,还是有一打(12个)reducer 。如果我们有多个 reducer ,Redux 能帮我们合并成一个。

让我们来定义 2 个 reducer

var userReducer = function (state = {}, action) {
    console.log('userReducer was called with state', state, 'and action', action)

    switch (action.type) {
        // etc.
        default:
            return state;
    }
}
var itemsReducer = function (state = [], action) {
    console.log('itemsReducer was called with state', state, 'and action', action)

    switch (action.type) {
        // etc.
        default:
            return state;
    }
}

我希望你特别留意赋给每个 reducer 的初始 state :

  1. 赋给 userReducer 的初始 state 是一个空对象,即 {}

  2. 赋给 itemsReducer 的初始 state 是一个空数组,即 []

赋予不同类型的值是为了说明 reducer 是可以处理任何类型的数据结构的。你完全可以选择那些符合你的需求的数据结构作为 state 的值。(例如,字面量对象、数组、布尔值、字符串或其它不可变结构)

在这种多个 reducer 的模式下,我们可以让每个 reducer 只处理整个应用的部分 state 。但我们需要知道,createStore 只接收一个 reducer 函数。

那么,我们怎么合并所有的 reducer? 我们又该如何告诉 Redux 每个 reducer 只处理一部分 state 呢? 其实这很简单。我们使用 combineReducers 辅助函数。combineReducers 接收一个对象并返回一个函数,当 combineReducers 被调用时,它会去调用每个reducer,并把返回的每一块 state 重新组合成一个大 state 对象(也就是 Redux 中的 Store)。 长话短说,下面演示一下如何使用多个 reducer 创建一个 Redux 实例:

import { createStore, combineReducers } from 'redux'

var reducer = combineReducers({
    user: userReducer,
    items: itemsReducer
})
// 输出:
// userReducer was called with state {} and action { type: '@@redux/INIT' }
// userReducer was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_9.r.k.r.i.c.n.m.i' }
// itemsReducer was called with state [] and action { type: '@@redux/INIT' }
// itemsReducer was called with state [] and action { type: '@@redux/PROBE_UNKNOWN_ACTION_4.f.i.z.l.3.7.s.y.v.i' }
var store_0 = createStore(reducer)
// 输出:
// userReducer was called with state {} and action { type: '@@redux/INIT' }
// itemsReducer was called with state [] and action { type: '@@redux/INIT' }

正如你从输出中看到的,每个 reducer 都被正确地调用了(但接收了个 init action @@redux/INIT )。 这个 action 是什么鬼?

这是 combineReducers 实施的一次安全检查,用以确保 reducer 永远不会返回undefined。请注意,在 combineReducers 中第一次调用 init action 时,其实是随机 action 来的,但它们有个共同的目的 (即是做一个安全检查)。

console.log('store_0 state after initialization:', store_0.getState())
// 输出:
// store_0 state after initialization: { user: {}, items: [] }

有趣的是,我们发现 Redux 正确处理了 state 的各个部分。最终的 state 完全是一个简单的对象,由userReducer 和 itemsReducer 返回的部分 state 共同组成。

{
    user: {}, // {} is the slice returned by our userReducer
    items: [] // [] is the slice returned by our itemsReducer
}

由于我们为每个 reducer 初始化了一个特殊的值(userReducer 的是空对象 {} ,itemsReducer 的是空数组 [] ),所以在最终 Redux 的 state 中找到那些值并不是巧合。

现在,关于 reducer 如何工作我们已经有了清楚的理解。是时候去看看当 action 被分发(dispatch)时会对Redux 的 state 有什么影响。

// 继续下一个教程: 06_dispatch-action.js

Dispatch action

迄今为止我们的关注点都是绑定我们的 reducer,但我们还未 dispatch 任何一个 action。我们将会用到上一章的 reducer ,并用它们处理一些 action:

var userReducer = function (state = {}, action) {
    console.log('userReducer was called with state', state, 'and action', action)

    switch (action.type) {
        case 'SET_NAME':
            return {
                ...state,
                name: action.name
            }
        default:
            return state;
    }
}
var itemsReducer = function (state = [], action) {
    console.log('itemsReducer was called with state', state, 'and action', action)

    switch (action.type) {
        case 'ADD_ITEM':
            return [
                ...state,
                action.item
            ]
        default:
            return state;
    }
}

import { createStore, combineReducers } from 'redux'

var reducer = combineReducers({
    user: userReducer,
    items: itemsReducer
})
var store_0 = createStore(reducer)


console.log("\n", '### It starts here')
console.log('store_0 state after initialization:', store_0.getState())
// 输出:
// store_0 state after initialization: { user: {}, items: [] }

让我们来 dispatch 我们的第一个 action… 记住在 ‘simple-action-creator.js’ 中所提到的: 为了 dispatch 一个 action,我们需要一个 dispatch 函数。

我们所看到的 dispatch 函数,是 Redux 提供的,并且它会将 action 传递给任何一个 reducer!dispatch 函数本质上是 Redux 的实例的属性 “dispatch”

dispatch 一个 action:

store_0.dispatch({
    type: 'AN_ACTION'
})
// 输出:
// userReducer was called with state {} and action { type: 'AN_ACTION' }
// itemsReducer was called with state [] and action { type: 'AN_ACTION' }

每一个 reducer 都被调用了,但是没有一个 action type 是 reducer 需要的, 因此 state 是不会发生变化的:

console.log('store_0 state after action AN_ACTION:', store_0.getState())
// 输出:store_0 state after action AN_ACTION: { user: {}, items: [] }

但是,等一下!我们是不是可以用一个 action creator 去发送一个 action?我们确实可以用一个 actionCreator,但由于它只是返回一个 action,那么就意味着它不会携带任何东西到这个例子中。但为了面对未来遇到的困难,我们还是以正确的方式,即以 flux 理论去做吧。让我们使用这个 action creator 发送一个我们想要的 action:

var setNameActionCreator = function (name) {
    return {
        type: 'SET_NAME',
        name: name
    }
}

store_0.dispatch(setNameActionCreator('bob'))
// 输出:
// userReducer was called with state {} and action { type: 'SET_NAME', name: 'bob' }
// itemsReducer was called with state [] and action { type: 'SET_NAME', name: 'bob' }

console.log('store_0 state after action SET_NAME:', store_0.getState())
// 输出:
// store_0 state after action SET_NAME: { user: { name: 'bob' }, items: [] }

我们刚刚处理了一个 action,并且它改变了应用的 state!但是这似乎太简单了,并且还不足以充当一个真实的用例。例如,如果我们要在 dispatch action 之前做一些异步的操作,那应该怎么做呢?我们将在下一章节 “dispatch-async-action.js” 中讨论这个问题。

至止,我们接触的应用流程是这样的:

ActionCreator -> Action -> dispatcher -> reducer

Dispatch async action 1

在上节教程中我们知道了如何分发 action 以及这些 action 如何通过 reducer 函数修改应用状态。

但是,到目前为止,我们只考虑了一种情况,同步场景下的 action,准确地说是同步 action creator,它创建同步的 action,也就是当 action creator 被调用时,action 会被立即返回。

我们来设想一个简单的异步场景:

  1. 用户点击“Say Hi in 2 seconds”按钮
  2. 当用户点击按钮 A,我们希望经过两秒,视图显示一条消息 Hi
  3. 两秒过去之后,更新视图,显示消息 Hi

当然这条消息是应用的状态之一,所以我们必然将其存储于 Redux store。但是我们希望的结果是,在调用 action creator 的两秒之后才把消息存入 store(因为如果立即更新状态,那么就会立即触发所有监听状态变更的订阅者 —— 例如视图,导致消息早于两秒显示)。

如果我们按照目前调用 action creator 的方式…

import { createStore, combineReducers } from 'redux'

var reducer = combineReducers({
    speaker: function (state = {}, action) {
        console.log('speaker was called with state', state, 'and action', action)

        switch (action.type) {
            case 'SAY':
                return {
                    ...state,
                    message: action.message
                }
            default:
                return state;
        }
    }
})
var store_0 = createStore(reducer)

var sayActionCreator = function (message) {
    return {
        type: 'SAY',
        message
    }
}

console.log("\n", 'Running our normal action creator:', "\n")

console.log(new Date());
store_0.dispatch(sayActionCreator('Hi'))

console.log(new Date());
console.log('store_0 state after action SAY:', store_0.getState())

// 输出(忽略初始输出):
//     Sun Aug 02 2015 01:03:05 GMT+0200 (CEST)
//     speaker was called with state {} and action { type: 'SAY', message: 'Hi' }
//     Sun Aug 02 2015 01:03:05 GMT+0200 (CEST)
//     store_0 state after action SAY: { speaker: { message: 'Hi' } }

结果 store 被立即更新了。我们希望看到的结果应该类似于下面这样的代码:

var asyncSayActionCreator_0 = function (message) {
    setTimeout(function () {
        return {
            type: 'SAY',
            message
        }
    }, 2000)
}

但是这样 action creator 返回的不是 action 而是 undefined。所以这并不是我们所期望的解决方法。

这里有个诀窍:不返回 action,而是返回 function。这个 function 会在合适的时机 dispatch action。但是如果我们希望这个 function 能够 dispatch action,那么就需要向它传入 dispatch 函数。于是代码类似如下:

var asyncSayActionCreator_1 = function (message) {
    return function (dispatch) {
        setTimeout(function () {
            dispatch({
                type: 'SAY',
                message
            })
        }, 2000)
    }
}

你可能再次注意到 action creator 返回的不是 action 而是 function。所以 reducer 函数很可能不知道如何处理这样的返回值,而你也并不清楚是否可行,那么让我们一起再做尝试,一探究竟。

运行之前我们在 dispatch-async-action-1.js 中实现的第一个异步 action creator:

import { createStore, combineReducers } from 'redux'

var reducer = combineReducers({
    speaker: function (state = {}, action) {
        console.log('speaker was called with state', state, 'and action', action)

        switch (action.type) {
            case 'SAY':
                return {
                    ...state,
                    message: action.message
                }
            default:
                return state;
        }
    }
})
var store_0 = createStore(reducer)

var asyncSayActionCreator_1 = function (message) {
    return function (dispatch) {
        setTimeout(function () {
            dispatch({
                type: 'SAY',
                message
            })
        }, 2000)
    }
}

console.log("\n", 'Running our async action creator:', "\n")
store_0.dispatch(asyncSayActionCreator_1('Hi'))

// 输出:
//     ...
//     /Users/classtar/Codes/redux-tutorial/node_modules/redux/node_modules/invariant/invariant.js:51
//         throw error;
//               ^
//     Error: Invariant Violation: Actions must be plain objects. Use custom middleware for async actions.
//     ...

我们所设计的 function 似乎没有进入 reducer 函数。但是 Redux 给出了温馨提示:使用自定义中间件(middleware)来支持异步 action。看来我们的方向是正确的,可中间件(middleware)又是什么呢?

我向你保证 action creator asyncSayActionCreator_1 不仅没有问题,而且只要我们搞清楚 middleware 的概念并掌握它的使用方法,这个异步 action creator 就会按照我们所设想的结果执行。

Middleware

在上章节中我们抛出了“中间件”的概念。中间件似乎可以帮助我们处理异步 action。但中间件到底是什么呢? >通常来说中间件是在某个应用中 A 和 B 部分中间的那一块,中间件可以把 A 发送数据到 B 的形式从 A -----> B 变成: A ---> middleware 1 ---> middleware 2 ---> middleware 3 --> ... ---> B

那么中间件在 Redux 中是如何工作的? 看上去 Redux 并不能自动处理 action creator 中返回的异步函数。但如果在 action creator 和 reducer 之间增加一个中间件,就可以把这个函数转成适合 Redux 处理的内容:

action ---> dispatcher ---> middleware 1 ---> middleware 2 ---> reducers

每当一个 action(或者其他诸如异步 action creator 中的某个函数)被分发时,我们的中间件就会被调用,并且在需要的时候协助 action creator 分发真正的 action(或者什么都不做,有时我们需要这么做)

在 Redux 中,中间件是纯粹的函数,有明确的使用方法并且严格的遵循以下格式:

var anyMiddleware = function ({ dispatch, getState }) {
    return function(next) {
        return function (action) {
             // 你的中间件业务相关代码
        }
    }
}

如上所述,中间件由三个嵌套的函数构成(会依次调用):

  1. 第一层向其余两层提供分发函数和 getState 函数(因为你的中间件或 action creator 可能需要从 state 中读取数据)
  2. 第二层提供 next 函数,它允许你显式的将处理过的输入传递给下一个中间件或 Redux(这样 Redux 才能调用所有 reducer)。
  3. 第三层提供从上一个中间件或从 dispatch 传递来的 action, 这个 action 可以调用下一个中间件(让 action 继续流动) 或者以想要的方式处理 action。

学习过函数式编程的人可能会意识到给上述代码提供了一个机会来使用”柯里化”(如果你不理解也没关系,跳过接下去的 10 行,不会影响你对 redux 的理解)。 使用柯里化,你可以简化上述函数:

// "curry" may come any functional programming library (lodash, ramda, etc.)
var thunkMiddleware = curry(
       ({dispatch, getState}, next, action) => (
          // 你的中间件业务相关代码
        )
    );

我们为异步 action creator 提供的中间件叫 thunk middleware它的代码在:https://github.com/gaearon/redux-thunk.它看上去是这样 (为了可读性使用 ES5 语法书写该函数):

var thunkMiddleware = function ({ dispatch, getState }) {
    // console.log('Enter thunkMiddleware');
    return function(next) {
        // console.log('Function "next" provided:', next);
        return function (action) {
            // console.log('Handling action:', action);
            return typeof action === 'function' ?
                action(dispatch, getState) :
                next(action)
        }
    }
}

为了让 Redux 知道我们有一个或多个中间件,我们使用 Redux 的辅助函数:applyMiddleware。

applyMiddleware 接收所有中间件作为参数,返回一个供 Redux createStore 调用的函数。当最后这个函数被调用时,它会产生一个 Store 增强器,用来将所有中间件应用到 Store 的 dispatch 上。参考

下面就是如何将一个中间件应用到 Redux store:

import { createStore, combineReducers, applyMiddleware } from 'redux'

const finalCreateStore = applyMiddleware(thunkMiddleware)(createStore)
// 针对多个中间件, 使用:applyMiddleware(middleware1, middleware2, ...)(createStore)

var reducer = combineReducers({
    speaker: function (state = {}, action) {
        console.log('speaker was called with state', state, 'and action', action)

        switch (action.type) {
            case 'SAY':
                return {
                    ...state,
                    message: action.message
                }
            default:
                return state
        }
    }
})

const store_0 = finalCreateStore(reducer)
// 输出:
//     speaker was called with state {} and action { type: '@@redux/INIT' }
//     speaker was called with state {} and action { type: '@@redux/PROBE_UNKNOWN_ACTION_s.b.4.z.a.x.a.j.o.r' }
//     speaker was called with state {} and action { type: '@@redux/INIT' }

// 现在 store 的 middleware 已经准备好了,再来尝试分发我们的异步 action:

var asyncSayActionCreator_1 = function (message) {
    return function (dispatch) {
        setTimeout(function () {
            console.log(new Date(), 'Dispatch action now:')
            dispatch({
                type: 'SAY',
                message
            })
        }, 2000)
    }
}

console.log("\n", new Date(), 'Running our async action creator:', "\n")

store_0.dispatch(asyncSayActionCreator_1('Hi'))
// 输出:
//     Mon Aug 03 2015 00:01:20 GMT+0200 (CEST) Running our async action creator:
//     Mon Aug 03 2015 00:01:22 GMT+0200 (CEST) 'Dispatch action now:'
//     speaker was called with state {} and action { type: 'SAY', message: 'Hi' }

当我们调用异步 action creator 两秒之后,action 成功被分发出去。

你可能会好奇,一个中间件如何 log 出所有已分发的 action ,是这样:

function logMiddleware ({ dispatch, getState }) {
    return function(next) {
        return function (action) {
            console.log('logMiddleware action received:', action)
            return next(action)
        }
    }
}

同样的,下面是一个中间件,它会丢弃所有经过的 action(不是很实用,但是如果加一些判断就能实现丢弃一些 action,放到一些 action 给下一个中间件):

function discardMiddleware ({ dispatch, getState }) {
    return function(next) {
        return function (action) {
            console.log('discardMiddleware action received:', action)
        }
    }
}

通过使用 logMiddleware 或 discardMiddleware 试着修改上述的 finalCreateStore 调用看看会发生什么… 比如,这样用:

const finalCreateStore = applyMiddleware(discardMiddleware, thunkMiddleware)(createStore)

会让你的 action 永远无法到达 thunkMiddleware 和 reducer。查看 http://rackt.org/redux/docs/introduction/Ecosystem.html 的中间件部分可以了解其他例子。

总结一下到目前为止我们所学的:

  1. 我们知道怎样写 action 和 action creator
  2. 我们知道怎样分发 action
  3. 我们知道怎样使用中间件处理自定义 action,比如异步 action

对于 Flux 体系的完整闭环,我们还剩下唯一的一块就是如何订阅 state 的更新并响应这些更新(比如重新渲染我们的组件)

所以我们怎么订阅 Redux store 的更新呢?

State subscriber

我们接近完成一个完整的 Flux 闭环了,现在只差一个至关重要的环节:

 _________      _________       ___________
|         |    | Change  |     |   React   |
|  Store  |----▶ events  |-----▶   Views   |
|_________|    |_________|     |___________|

没有它,在 store 改变时我们就不能更新我们的视图。

幸运的是,监视 Redux store 更新有一个很简单的办法:

store.subscribe(function() {
   // retrieve latest store state here
   // Ex:
   console.log(store.getState());
})

是的,简单到我们都开始重新相信圣诞老人了(译者注:2333,对不起这个比喻太幽默了)

试一下这段代码:

import { createStore, combineReducers } from 'redux'
var itemsReducer = function (state = [], action) {
    console.log('itemsReducer was called with state', state, 'and action', action)

    switch (action.type) {
        case 'ADD_ITEM':
            return [
                ...state,
                action.item
            ]
        default:
            return state;
    }
}

var reducer = combineReducers({ items: itemsReducer })
var store_0 = createStore(reducer)

store_0.subscribe(function() {
    console.log('store_0 has been updated. Latest store state:', store_0.getState());
    // 在这里更新你的视图
})

var addItemActionCreator = function (item) {
    return {
        type: 'ADD_ITEM',
        item: item
    }
}

store_0.dispatch(addItemActionCreator({ id: 1234, description: 'anything' }))

// 输出:
//     ...
//     store_0 has been updated. Latest store state: { items: [ { id: 1234, description: 'anything' } ] }

我们的订阅回调成功的调用了,同时 store 现在包含了我们新增的条目。

理论上,到这就可以停止了。我们的 Flux loop 已经闭合,我们理解了构造 Flux 的全部概念,实际上它也没那么神秘。但是老实说,还有很多要讲的,为了让最后一个概念保持简单, 我们有意的在例子中去掉了一些东西:

  • 我们的订阅回调没有把 state 作为参数,为什么?
  • 既然我们没有接受新的 state, 我们就被限定到了只能开发这个已经完成的 store (store_) 所以这个办法在含有多个模块的应用下不可行。
  • 我们究竟是怎么更新视图的?
  • 怎么取消订阅?
  • 更通俗的讲,我们怎么把 Redux 和 React 结合到一起?

我们现在进入了一个“将 Redux 加入到 React” 的领域。理解 Redux 可以无条件绑定到 React 上是很重要的。 Redux 是一个“为 Javascript 应用而生的可预测的状态容器”,你有很多方式去使用它,而 React 应用只不过是其中一个。从这个角度看,如果没有 react-redux,我们将失去很多。 在 Redux 1.0.0 之前它是包含在 Redux 中的,这个库节省了我们很多时间,它包含了在 React 中使用 Redux 时所有的绑定。

回到订阅这件事,为什么我们这个订阅函数看上去非常简单而且没有提供很多特性?

这就是 Redux 精彩之处了! 它所有 API 都很抽象(包括订阅),支持高度扩展,允许开发者造出一些疯狂的轮子比如 Redux DevTools

但是最后我们还是需要一个更好的接口订阅我们的 store 变化。这也就是 react-redux 给带给我们的: 一个完美填补原生 Redux 订阅机制和开发者的期待之间的空缺的 API ,这样我们不再需要直接使用订阅。而只是使用 “provide” 和 “connect” 绑定,不必再关心隐含在内的订阅方法。所以,订阅方法依然会被我们使用,只不过它通过高度整合的接口替我们处理 redux state 的连接。

现在我们隐藏了那些绑定,并且展示了连接你的组件和 Redux 的 state 是很轻松的一件事。

Provider and connect

这其实是教程的最后一章,一起聊聊如何把 Redux 和 React 绑定在一起。

要运行下面的示例,你需要一个浏览器。

开发一个 React 应用和服务器来让浏览器可以访问,我们会用到:

  • node HTTP 创建一个非常简单的服务器
  • 用 Webpack 去打包我们的应用,
  • 神奇的 Webpack Dev Server作为一个专门的 node 服务器,并监听 JS 改变自动编译
  • 超棒的 React Hot Loader (Dan Abramov开发的另一个很棒的项目,没错,他就是 Redux 的作者) ,提供非常棒的DX (开发体验) ,当我们在编辑器中修改代码时,在浏览器中可以热加载来显示效果。

提醒一下正在使用 React 的开发者:本应用是基于 React 0.14 构建的我不想在这里详细地解释如何设置 Webpack Dev Server 和 React Hot Loader,因为在 React Hot Loader 的文档中已经说的很好了。

import webpackDevServer from './11_src/src/webpack-dev-server'
// 我们应用启动的主要服务器请求都是来自这个文件。
import server from './11_src/src/server'

// 如果 5050 端口号已经被占用了,那么就修改下面的端口号。
// 如果端口号是 X,那么我们可以用 X 作为服务器的端口号,用 X+1 作为 webpack-dev-server 的端口号
const port = 5050

// 启动 webpack dev server...
webpackDevServer.listen(port)
// ... 还有主应用服务器。
server.listen(port)

console.log(`Server is listening on http://127.0.0.1:${port}`)

以下是完整代码:

index.jsx

// 这个文件是我们 JS 包的入口。 在这里将创建我们的 Redux store,实例化我们的 React 应用根组件然后将它附加到DOM中。

import React from 'react'
import { render } from 'react-dom'
// 所有创建 store 的具体代码在 ./create-store.js 中
import createStore from './create-store'
// Application 是我们应用的根组件,它包含了 Redux 的 Provider...
import Application from './application'

// 就像以前的很多例子一样,我们需要创建 Redux 实例。 这次所有的代码被移到专门的模块中(译者注: create-store.js),并返回一个函数来触发实例化。
const store = createStore()

// 现在,是时候使用 ReactDOM.render(或者仅仅用 render,这要感谢 ES6 的解构赋值写法: import { render } from 'react-dom')渲染我们的应用到 DOM 中
render(
    // ... 接着将我们的 Redux store 作为 props 提供给根组件,于是 Redux Provider 可以做它该做的事情。
    <Application store={store} />,
    document.getElementById('app-wrapper')
)

// 转到 ./create-store.js 去浏览你已深知的一点: "如何创建一个 Redux store?"

create-store.js

// 章节 12 - Provider-and-connect.js

// 这里没有很多要说的, 现在你已经看到过这些很多次, 而且应该对它们很熟悉了...

// 尽管这样,但还是有一点要注意: 我们这里不使用之前用过的 thunk middleware。
// 替而代之的是 promise middleware,它允许我们处理异步的 action 创建函数,
// 然后漂亮的实时处理UI更新(也可以做一些乐观的更新)。
// 这个中间件在 https://github.com/reactjs/redux/issues/99有讨论,
// 在 react-redux-universal-example: https://github.com/erikras/react-redux-universal-hot-example中有非常好的使用案例,
// 我强烈推荐你去看一看(之后不是现在;))。

import { createStore, applyMiddleware, combineReducers } from 'redux'
// 你可以去看一看这个中间件,它不是很复杂,而且能帮你锻炼出对中间件更敏锐的理解。
import promiseMiddleware from './promise-middleware'

// 在本应用中我们仅有一个 reducer,
// 但是下面用 ES6 写的 import,有趣且一气呵成的导入并生成了多个 reducer。
// 去 ./reducers.js 看看我们的 reducer 究竟是怎么做的(这里没有魔法)。
import * as reducers from './reducers'

// 这里看到的 data 参数是用于初始化 Redux store。
// 为简单起见我们不讨论它,但要感谢它让你在有真实数据的情况下来初始化 reducer。
// 例如在一个同构/通用应用中,你能从服务器端拉取数据,然后序列化并传递到客户端,
// 你的 Redux store 就能用这些数据来初始化。
// 这里我们没传任何数据,但最好要知道这个 createStore 是做什么的
export default function(data) {
  var reducer = combineReducers(reducers)
  var finalCreateStore = applyMiddleware(promiseMiddleware)(createStore)
  var store = finalCreateStore(reducer, data)

  return store
}

// 转到 ./application.jsx,去学习使用 Provider 组件首次将 Redux 绑定到 React。

promise-middleware.js

  
export default function promiseMiddleware() {
  return (next) => (action) => {
    const { promise, types, ...rest } = action

    if (!promise) {
      return next(action)
    }

    const [REQUEST, SUCCESS, FAILURE] = types

    next({...rest, type: REQUEST})

    return promise().then(
      (result) => {
        next({...rest, result, type: SUCCESS})
      },
      (error) => {
        next({...rest, error, type: FAILURE})
      }
    )
  }
}

reducers.js

// 章节 12 - Provider-and-connect.js
//reducers.js
// 这个文件包含我们应用仅有的一个 reducer。 它的表现对于你来说没什么新鲜的,除了将一个 action(GET_TIME) 的3个方面,写成3个专用的 action...
// 这样做允许我们做很漂亮的实时UI更新,就像这样:
// 1) 当收到 GET_TIME_REQUEST action,我们修改 state 来告诉 UI 的一部分需要被冻结(因为有一个挂起的操作)
// 2) 当收到 GET_TIME_SUCCESS (或 GET_TIME_FAILURE)之后,我们修改 state 为不冻结应用程序,然后添加收到的新数据。

var initialTimeState = {}

// 下面的 reducer 命名用"_"开头,用于从 state 中读取的时候,避免 state.time.time (出现两个 time )。
// 这只是个人偏好你可以不必这样做,它取决于你如何对各个 reducer 命名,和在 Redux 的 store 中暴露哪些属性。
export function _time(state = initialTimeState, action) {
  console.log('_time reducer called with state ', state , ' and action ', action);

  switch (action.type) {
    case 'GET_TIME_REQUEST':
      return {
        ...state,
        frozen: true
      }
    case 'GET_TIME_SUCCESS':
      return {
        ...state,
        time: action.result.time,
        frozen: false
      }
    case 'GET_TIME_FAILURE':
        // 这里我们可以添加一个错误消息,打印到我们应用程序的某个地方
      return {
        ...state,
        frozen: false
      }
    default:
      return state
  }
}

application.jsx

// 现在是时候见识 redux-react(https://github.com/rackt/react-redux)
// 如何在 Provider 组件中为我们做初次绑定了。

// Provider 是一个 React 组件,它被设计用作于包裹你应用的根组件。
// 它的目的是提供你的 Redux 实例给所有应用中的组件。
// 我们不太关心它是如何做到的,你只需知道: 它用了 React 的上下文功能(context feature),
// 它没有说明文档所以不必在意它,但你实在是好奇的话可以参考: https://www.tildedave.com/2014/11/15/introduction-to-contexts-in-react-js.html

import React from 'react'
import Home from './home'
import { Provider } from 'react-redux'

export default class Application extends React.Component {
  render () {
    return (
      // 正如上面介绍的,Provider 必须包裹你的应用程序根组件。
      // 如此以来,该组件以及它的子元素(甚至更深的后代)就能访问你的 Redux store。
      // 当然,为了允许 Provider 这么做,你必须通过名为 "store" 的 props 将 store 传递给它。
      <Provider store={ this.props.store }>
        <Home />
      </Provider>
    )
  }
}

// 转到 ./home.jsx,从这个React组件中,去发现更多关于 state 和派发 action 的细节。

home.jsx

// 到这为止我们的教程就基本上结束了, 关于 Redux 唯一剩下的问题
// 就是我们如何能读取 store 里面的 state 以及如何进行action的分发。

// 这两个问题都能够使用 Connect 去解决。

// 正如我们前面所讲解的, 当我们使用 Provider 组件时, 我们允许我们应用中的所有组件访问Redux.
// 但是这种访问只能使用不正式的特性 React's context 来实现,
// 为了避免这种"黑科技"式 (不规范) 的 API 调用,
// React-Redux 为我们暴露了一个组件中的函数让我们可以使用。

// 这个函数就是 Connect , 它让我们可以实现一个组件和 Redux store 的绑定,
// 通过这种绑定可以让store通过组件的属性 (prop) 分发函数,
// 也可以根据我们自己的需要增加任何需要暴露的属性作为store里面state的一部分。

// 使用了 Connect , 你可以通过添加很少的代码让一个组件变得更"聪明",
// (https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0).

// Connect 是一个接受一些映射函数作为参数, 并返回一个你想要链接的组件类函数, 的函数.
// 这样的函数被叫做高层组件 (Higher Order Component (HOC))
// HOC 函数是一种在不修改继承的情况下能够添加特性和行为到输入 (component, store) 中的函数模式。
// 这种方式比较继承更强调构成,
// 这也是创建 React 应用更建议的方式 (其实并没有这个限制)
// 阅读更多关于 HOC 和 composition 的文档:
// - https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.lpp7we7mx
// - http://natpryce.com/articles/000814.html

// Connect "HOC" 主要被设计用于解决无论简单和困难的使用场景。
// 在现有的例子中, 我们不会使用 Connect 最复杂的形式,
// 但是你可以在完整的 API 文档中找到有关的全部信息:
// https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options

// 以下是完整的 Connect 特征:
// connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
// 它的使用方法如下:

/*
  const wrappedComponentClass = connect(...)(ComponentClass)
*/

// 我们在这里会主要讲解 Connect 的第一个参数: mapStateToProps

// Connect 将一个选择你想要将哪一些 state 暴露给组件的函数作为第一个参数。
// 这个函数我们一般称它为 Selector。
// 它需要接受两个参数: 当前 store 的状态 (state) 以及当前组件的 prop。
// 可以看见我们将这个函数命名为 "mapStateToProps",
// 这个名字从字面意义上告诉我们它的作用:
// 它创造了从 state 到一些组件 props 的映射 (map)

// 为了完成提取部分组建的 props 作为 state 的动作, (Ex: state.items[props.someID]).
// 组件的 props 也被作为参数提供。
// "mapStateToProps" 被期望返回所有你希望暴露给你的组件的 props, 通常通过一个对象常量 (object literal)返回。
// 你可以在返回前修改取得的 state 值。下面是一些简单的 Connect 应用 (在定义组件之后)

import React from 'react'
import { connect } from 'react-redux'
// 我们会使用一些 ES6 中的 import 技巧来得到所有的 action creator 并生成一个哈希值,
// 就跟我们当时在 reducer 部分所做的一样。如果你还没有了解action creator的话, 去看看相关章节吧~  (./action-creators.js).

import * as actionCreators from './action-creators'

class Home extends React.Component {
  onTimeButtonClick (delay) {
    // 这个按钮处理器在用户的点击事件后会分发一个 action。
    // 我们在这里会使用一个 Connect 提供的分发函数,
    // 也有很多其他的调用被绑定到分发器的 actionCreator 的方式,
    // 这种方式提供了第二个 Connect 的参数:
    // https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options
    // 被传到 actionCreators.getTime 的 delay 值是为了在我们能得到当前时间之前模拟异步的工作,
    // 试着修改这个值来正确影响我们的 UI
    this.props.dispatch(actionCreators.getTime(delay))
  }
  render () {

    // 因为 Connect 我们能够通过 props 取到特定的数据
    var { frozen, time, reduxState } = this.props
    var attrs = {}
    const DELAY = 500 // 毫秒

    if (frozen) {
        attrs = {
          disabled: true
        }
    }

    return (
      <div>
        <h1>Provider and connect example</h1>
        <span>
          <b>What time is it?</b> { time ? `It is currently ${time}` : 'No idea yet...' }
        </span>
        <br /> <br />
        <i>
          When clicking the button below, the time will be provided after a {DELAY}ms delay.<br />
          Try to change this value (in <b>src/home.jsx - line 95</b>) to verify that the delay given correctly impacts our UI.
        </i>
        <br />
        {/* 这里注册按钮的 "onClick" 句柄: */}
        <button { ...attrs } onClick={() => this.onTimeButtonClick(DELAY)}>Get time!</button>
        <pre>
          redux state = { JSON.stringify(reduxState, null, 2) }
        </pre>
      </div>
    )
  }
}

// 这是我们的 select 函数, 它会把我们需要在属性 (prop) 中对我们的组件暴露的数据从 state 中抽离出来
const mapStateToProps = (state/*, props*/) => {
  return {
    frozen: state._time.frozen,
    time: state._time.time,
    // 像 (reduxState: state) 这样提供整个 state 是一种不好的实现,
    // 我们在这里这样写是为了让大家能看到我们页面字符串化的结果。更多信息请访问以下链接:
    // https://github.com/reactjs/react-redux/blob/master/docs/api.md#inject-dispatch-and-every-field-in-the-global-state
    reduxState: state,
  }
}

const ConnectedHome = connect(mapStateToProps)(Home)

export default ConnectedHome

// 也许你会发现因为 Redux 让我们可以拥有需要一些状态 (比如保存当前时间) 的动态组件,
// 而这个状态并不在组件当中存在,
// 我们的组件只会接受含有需要的数据的 prop。
// 我们现在拥有了一个无状态组件 (stateless component), 相比较有状态的组件,
// 我们在编码中应该尽可能更多的使用无状态组件, 因为它们更易于被复用。
// 正如在 "onTimeButtonClick" 处理器中建议的一样, 我们甚至可以把点击的回调函数作为一个 prop
// 从 Connect 的第二个参数 "mapDispatchToProps" 中传入。这么做的话, 我们就可以在组件之外获得按钮的行为,
// 通过允许另一种点击行为让我们更易于重用这个按钮。
// 可复用性的概念也许看起来被过度强调了, 但是拥有一个可复用的组件通常也意味着
// 这个组件能够很简单的被测试 (因为你可以将任何你想要的数据和处理器插入你的组件中,
// 从而很简单的保证它的正确运行)

// 在去看 ./12_final-words.js 之前, 请仔细看以下另一种使用 Connect HOC 的方法。

// 因为 Connect 返回了一个接受一个 class 并返回另一个 class 的函数,
// 我们可以用它作为 ES7 的 decorator。Decorator 是一种 ES7 的实验新特性,
// 让我们能够在设计的时候注释和修改 class 和属性 (https://github.com/wycats/javascript-decorators).

// 作为一个试验中的特性, 也许会有变化或问题。
// 如果你选择从现在开始使用的话, 请注意和接受今后的不确定性。
// Decorator 提供了一些代码的语法糖让我们的代码变得稍稍不同。原来的这种写法:

/*
  class MyClass {}
  export default somedecorator(MyClass)
*/

// 你可以这么写:

/*
  @somedecorator
  export default class MyClass {}
*/

// 通过这种特性使用 redux connect, 我们可以把如下代码:

/*
 let mapStateToProps = (state) => { ... }
 @connect(mapStateToProps)
 export default class MyClass {}
 */

// 替换成:

/*
  let mapStateToProps = (state) => { ... }
  class MyClass {}
  export default connect(mapStateToProps)(MyClass)
*/

// 正如我们看到的, 高层组件的函数现在被作为隐函数使用 ( @connect(mapStateToProps) )
// 而不是直接调用它们自身  ( @connect(mapStateToProps)(Myclass) )。 有些人觉得这种写法更加优雅,
// 而另一些则因为这种写法隐藏了实际发生的事情会当人无法理解 decorator 是如何工作的。
// 知道了这些并了解到 decorator 现在仍在实验阶段的话, 你就可以决定你偏好使用哪一种 Connect 的形式,
// 也不会因为在其他文章, 教学和新手包中看到两种语法而感到惊讶了

// 去 ./12_final-words.js 可以看到我们最后的一些建议

action-creators.js

// 章节 12 - Provider-and-connect.js

// 我们使用 Bluebird(https://github.com/petkaantonov/bluebird) 作为 promise 库,但其实你可以用任何你喜欢的。
import Promise from 'bluebird'

// 我们的 action 创建函数在一段延迟后获取当前时间,用于演示 promise 中间件的用法。

// promise 中间件接收2种情况的 action:
// 1) 一个如下格式的 action:
//    {
//      types: [REQUEST, SUCCESS, FAILURE], // action 的 types 需要按该顺序给出
//      promise: function() {
//        // 返回一个 promise
//      }
//    }
// 2) 其他任何可以传递给下一个中间件或 Redux 的 action, (准确的说,在这个 promise 中间件的实现中,这里的"其他任何 action" 传递到下一个中间件或 Redux 时,必须不包含 promise 属性)

// 当该 promise 中间件接收到 action 之后,它会生成2个 action:
// 一个 action 用于 action 创建函数的 REQUEST 情况,
// 后一个 action 用于 action 创建函数的 SUCCESS 或 FAILURE 情况

// 再者,这个 promise 中间件的代码并不复杂,值得去看一看 (./promise-middleware.js)

// 下面的 action 使用 "delay" 作为一个参数传递,用来延迟该 action 创建函数。
// 尝试改变延迟的值,验证它是否正确影响了我们 UI。
export function getTime(delay) {
  return {
    types: ['GET_TIME_REQUEST', 'GET_TIME_SUCCESS', 'GET_TIME_FAILURE'],
    promise: () => {
      return new Promise((resolve, reject) => {
        // 通过 setTimeout 来模拟一个异步服务器请求
        setTimeout(() => {
          const d = new Date()
          const ms = ('000' + d.getMilliseconds()).slice(-3)
          resolve({
            time: `${d.toString().match(/\d{2}:\d{2}:\d{2}/)[0]}.${ms}`
          })
        }, delay)
      })
    }
  }
}

Final words

除了本教程,其实 Redux 和 react-redux 还有很多可以学习的。比如:考虑到 Redux 的安全性,你可能会对 bindActionCreators 感兴趣(创建一个已绑定到 dispatch 的 action creators hash 对象 -http://rackt.org/redux/docs/api/bindActionCreators.html)。希望我们已经给了你一些秘诀去更好的理解 Flux ,以及 Flux 不同实现之间的区别–当然包括 Redux 是如何脱颖而出的 ;)。

接下去呢?

Redux 的官方文档真的非常出色和详尽,所以从现在开始不要再犹豫要不要看它了:http://rackt.org/redux/index.html

祝你和 React、Redux 玩的开心2333!

参考