关于redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。它可以配合如react,angular等框架进行使用。

为什么会出现Redux?

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。

这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 我称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 试图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux就是为了帮你解决这个问题。

先认识flux

Redux的实现是借鉴flux的思想,那么要学习Redux先认识flux可能是有必要的,下面是flux的一个单向数据流图。

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

flux和传统的mvc有什么区别?
通常准行mvc模式的网站又一下结构组成。

  • 模板/HTML = View
  • 填充视图的数据 = Model
  • 获取数据、将所有视图组装在一起、响应用户事件、数据操作等等的逻辑 = Controller
    而flux与其的概念非常相似,主要是在只是在某些表述上有些小小的不同:
  • Model 看起来像 Store
  • 用户事件、数据操作以及它们的处理程序看起来像 “action creators” -> action -> dispatcher -> callback
  • View 看起来像 React view (或者其它类似的概念)

为了弄清楚 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 -> …

flux的出现解决了前端传统mvc模式难以解决的问题。

Redux和flux区别在哪里?

跟随 Flux、CQRS 和 Event Sourcing 的脚步,通过限制更新发生的时间和方式,Redux 试图让 state 的变化变得可预测。这些限制条件反映在 Redux 的三大原则中。

  • 单一数据源
    所有的数据都保存在一个object stree中。(即state对象中)
  • State是只读的
    只能通过触发action来对store进行更改。(怎么触发anction,下一节会解释)
  • 使用纯函数来执行修改
    为了描述 action 如何改变 state tree ,你需要编写 reducers。(关于reducers下面会对其进行详解)

而Redux与flux最重要的不同是:

  1. Redux 并没有 dispatcher 的概念
    Redux依赖纯函数来替代事件处理器。Flux 常常被表述为 (state, action) => state。从这个意义上说,Redux 无疑是 Flux 架构的实现,且得益于纯函数而更为简单。(疑惑1:怎样才算纯函数?)
  2. Redux 设想你永远不会变动你的数据
    你可以很好地使用普通对象和数组来管理 state ,而不是在多个 reducer 里变动数据。正确且简便的方式是,你应该在 reducer 中返回一个新对象(配合object spread 运算符提案或者一些库Immutable)来更新 state。

根据数据流顺序,我们下面将从action开始说起。

如何创建一个action?

很明显,一个action首先需要有action creater来进行发起,而一个creater其实就相当于一个函数而已…

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

对,就是这么简单,但是这里要注意的是anction的格式,在flux中,约定action是一个拥有type属性的对象,当然,它还可以拥有其它属性,但是type是必须要拥有的,可以说这是一个统一的规范。

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

好了,上面代码目前来说并没有什么用处。
在实际的场景中,我们需要的是将 action 发送到某个地方,让关心它的人知道发生了什么,并且做出相应的处理。
我们将这个过程称之为“分发 action(Dispatching an action)”。

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

Redux实际中具体解决了哪些问题?

考虑到在实际应用中,我们不仅需要 action 告诉我们发生了什么,还要告诉我们需要随之更新数据。

这就让我们的应用变的棘手:

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

而 Redux 给了我们解答:

  • 如何在应用程序的整个生命周期内维持所有数据?
1
2
3
以你想要的方式维持这些数据,例如 JavaScript 对象、数组、不可变数据,等等。
我们把应用程序的数据称为状态。这是有道理的,因为我们所说的数据会随着时间的推移发生变化,这其实就是应用的状态。
但是我们把这些状态信息转交给了 Redux(还记得么?Redux 就是一个“容纳状态的容器”)。
  • 如何修改这些数据?

    1
    2
    3
    我们使用 reducer 函数修改数据(在传统的 Flux 中我们称之为 store)。
    Reducer 函数是 action 的订阅者。
    Reducer 函数只是一个纯函数,它接收应用程序的当前状态以及发生的 action,然后返回修改后的新状态(或者有人称之为归并后的状态)。
  • 如何把数据变更传播到整个应用程序?

    1
    使用订阅者来监听状态的变更情况。

总的来说,Redux提供了:

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

而我们把redux的实例,称之为store,点击查看createStore源码

import { createStore } from 'redux'

var store = createStore(() => {}) // createStore 函数必须接收一个能够修改应用状态的函数(即reducer函数)。

创建我们的reducer

你可能注意到,在flux中,只有store并没有reducer,那么redux的reducer和flux的store他们两个的区别是什么呢?
区别很简单:
Store 可以保存你的 data,而 Reducer 不能。
因此在传统的 Flux 中,Store 本身可以保存 state,但在 Redux 中,每次调用 reducer时,都会传入待更新的 state。这样的话,Redux 的 store 就变成了“无状态的 store” 并且改了个名字叫 Reducer。

现在我们在reducer函数中,做点事吧。

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接收到的参数有(state, action),在初始化应用时候,我们的reducer被调用了,并且Redux dispatch 了一个初始化的 action ({ type: ‘@@redux/INIT’ }),由于state没有被初始化,所以为undefined。

获取state

而在初始化过后,我们获取state或怎样呢?

console.log('store_1 state after initialization:', store_1.getState()) // store_1 state after initialization: undefined

getState是store对象的一个方法,用于获取我们的state,而结果和我们预想的一样为undefined。

那么现在我们来对state进行一下初始化。

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()) // {}

创建reducer

当然,我们的reducer作用并不是为了console.log这些东东。
调用reducer,只是为了响应一个派发来的 action 。
下面我们编辑一个用于响应“SAY_SOMETHING”的reducer。

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

值得注意的是:

  • 发起的动作type是flux约定俗成的。而value属性可自己进行变化,如在vuex(vue)和ngrx(angular)中用的是payload。
  • 如之前提到过的,redux和flux的一大区别是不修改state,而是返回新的state,并且保证redux返回了state,因为若发来的action不能进行响应时,需要返回原本的state,否则会导致state为空
  • 在处理合并state时,我们可借助ES6,7语法以及其它的一些库来对其进行操作,或者可自己手工进行浅拷贝合并。

这可能是我们reducer最基本的样子了,但在实际项目中,我们需要响应多个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;
    }
}

很明显,这样是很难进行维护的,幸运的是,Redux 不关心我们到底是只有一个 reducer,还是有12564721个reducer。
如果我们有多个 reducer,Redux 能帮我们合并成一个。那真是极好的。

// 让我们来定义 2 个 reducer

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
            }
        // etc.
        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
            ]
        // etc.
        default:
            return state;
    }
}

那么,我们怎么合并所有的 reducer?

我们使用 combineReducers 辅助函数。combineReducers 接收一个对象并返回一个函数,当 combineReducers 被调用时,它会去调用每个
reducer,并把返回的每一块 state 重新组合成一个大 state 对象(也就是 Redux 中的 Store)。

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

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

在 combineReducers 中第一次调用 init action 时,其实是随机 action 来的,但它们有个共同的目的 (即是做一个安全检查)。
由于我们为每个 reducer 初始化了一个特殊的值(userReducer 的是空对象 {} ,itemsReducer 的是空数组 [] ),所以在最终 Redux 的 state 中找到那些值并不是巧合。

到目前为止,我们都还没有得到一个新 state, 因为我们还没有真的派发过任何 action 。接下来我们发起anction,看看会有怎样的情况

发起一个action

store对象中,除了拥有getstate方法外,还定义了dispatch 函数,它会将 action 传递。

使用上节的例子,我们先获取当前的state。

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

然后我们定义一个actioner

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

发起action并传递。

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

可以看到,我们定义的两个reducer都被执行了。

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

我们刚刚处理了一个 action,并且它改变了应用的 state!
在实际项目中,我们会经常遇到异步action的情况,而此时,我们需要一个解决异步action的中间件。可参考reduxAPI文档applyMuddkeware

怎么检测store的变化

到目前位置,我们就差最后重要的一步了。

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

即如何在store改变时,更新我们的视图(view)呢?
而redux已经帮我们考虑到了,在store对象中,有一个很简单的办法。

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

哈哈,是不是非常惊喜呢。
我们结合上面来看看整体的例子。

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的变化了。
而此时可能会感到疑惑,为什么subscribe回调没有state参数呢?若其它模块组件的store发生变化,那么这里的订阅并没有反应,所以当前的订阅只能对这一个store有效。而如果需要把其应用到所有模块中,使得所有模块的数据统一到一个store来,并且在各模板中可以应用和订阅它,怎么办?那么这就是react-redux,angular-redux等领域要做的事了,redux只不过是一个为Javascript应用而生的可预测的状态容器。它可以应用到多个地方,这就是 Redux 精彩之处! 它所有 API 都很抽象(包括订阅),支持高度扩展,允许开发者造出一些疯狂的轮子,比如Redux DevTools

最后

若要对源码进行解读,可参考这个Redux进阶教程;