你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
Jade

深入到源码:解读 redux 的设计思路与用法 11个回复 专栏 @ Javascript

Jade 发布于 1 年前

前言

redux 是 facebook 提出的 flux 架构的一种优秀实现;而且不局限于为 react 提供数据状态处理。它是零依赖的,可以配合其他任何框架或者类库一起使用。要想配合 react ,还得引入 react-redux

redux 团队的野心比较大,并不想让 redux 局限于 react 生态链中的一环。他们让 redux 自身保持简洁以便适配各种场景,让社区发展出各种 redux-* 中间件或者插件,从而形成它自己的生态系统。

redux 的核心很简洁。这篇文章将专注于解读 redux 核心的设计思路,以 Isomorphism-react-todomvc 这个项目为示例。它很可能是目前实现最完备的 react/redux todomvc demo,包含 react/redux 服务端渲染、路由filter、websocket同步等。

可以猛戳 DEMO 地址。在控制台里能看到 redux-logger 中间件输出的 action 日志,它们清晰地反映了业务逻辑是怎样的 。如果有其他人在编辑 todolist,基于 websocket 服务端推送技术的支持,你也可以直接看到别人的操作过程。

理念与设计

为什么要有 action ?

每个 web 应用都至少对应一个数据结构,而导致这个数据结构状态更新的来源很丰富;光是用户对 view 的操作(dom 事件)就有几十种,此外还有 ajax 获取数据、路由/hash状态变化的记录和跟踪等。

来源丰富不是最可怕的,更可怕的是每个来源提供的数据结构并不统一。DOM 事件还好,前端可以自主控制与设计; ajax 获取的数据,其结构常常是服务端开发人员说了算,他们面对的业务场景跟前端并不相同,他们往往会为了自己的便利,给出在前端看来很随意的数据结构。

即便是最专业的服务端开发人员,给出最精准的 restful 数据,它也会包含 meta 数据,表明此次返回是否存在错误,如果存在错误,则提供错误信息。除非是 facebook 最近提出的 graphql + relay 模式,不然我们总得对各个来源的数据做一个前期处理。

我们得用专门的处理函数,在各个数据来源里筛选出我们真正需要的数据,不把那些无关紧要的、甚至是脏的数据污染了我们的全局数据对象。

这种对数据来源做萃取工作的函数,就叫 action。它叫这个名字,不是因为它「数据预处理」的功能,而是在 web 应用中所有的数据与状态的变化,几乎都来自「事件」。dom 事件,ajax 成功或失败事件,路由 change 事件, setTimeout 定时器事件,以及自定义事件。任意事件都可能产生需要合并到全局数据对象里的新数据或者线索。

action 跟 event (事件)并不等同。比如在表单的 keyup 事件中,我们只在 e.keyCode 等于回车键或者取消键时,才触发一类 action。dom 事件提供的数据是 event 对象,里面主要包含跟 dom 相关的数据,我们无法直接合并到全局数据对象里,我们只将感兴趣的部分传入 action 函数而已。

所以,是 event 响应函数里主动调用了 action 函数,并且传入它需要的数据。

为什么要有 reducer ?

action 仅仅是预处理,将脏数据筛选掉,它未必产生了可以直接合并到全局对象的数据与结构,它甚至可能只是提供了线索,表示「需要获取某某数据,但不在我这儿」。action 函数的设计,也为它「只提供线索」的做法提供了支持,action 函数必须返回一个带有 type 属性的 plain object

//actions.js
//添加 item 只需要一个 text 字符串数据
export function addItem(text) {
    return {
        type: 'ADD_ITEM',
        text
    }
}
//删除 item 只需要拿到它的 id
export function deleteItem(id) {
    return {
        type: 'DELETE_ITEM',
        id
    }
}

//删除所有已完成事项,不需要额外数据,只需要线索,线索就是 type
export function clearCompleted() {
    return {
        type: 'CLEAR_COMPLETED'
    }
}

如上所示,action 函数的设计理念如下:

  • action 的参数用来筛掉脏数据,调用 action 函数的人,有义务只传入它需要的数据
  • action 返回的 plain object 中包含属性为 type 的常量值
    • 表明这个对象里携带的其他数据应该被如何「再处理」
    • 或者不带其他数据,仅仅启示已有数据需要如何调整,或者需要主动获取哪些数据

reducer 就是迎接 action 函数返回的线索的「数据再处理函数」, action 是「预处理函数」。

因为 action 返回的数据有个固定的结构,所以 reducer 函数也有个固定结构。

//reducer 接受两个参数,全局数据对象 state 以及 action 函数返回的 action 对象
//返回新的全局数据对象 new state
export default (state, action) => {
    switch (action.type) {
        case A:
        return handleA(state)
        case B:
        return handleB(state)
        case C:
        return handleC(state)
        default:
        return state //如果没有匹配上就直接返回原 state
    }
}

如上所示,每个 action.type 的 case (A/B/C),都有一个专门对应的数据处理函数 (handleA/handleB/handleC),处理完之后返回新的 state 即可。

reducer 只是一个模式匹配的东西,真正处理数据的函数,是额外在别的地方写的,在 reducer 中调用罢了。

reducer 为什么叫 reducer 呢?因为 action 对象各种各样,每种对应某个 case ,但最后都汇总到 state 对象中,从多到一,这是一个减少( reduce )的过程,所以完成这个过程的函数叫 reducer

为什么要有 combineReducers ?

reducer 的第一个参数是全局 state 对象。你想想看,全局意味着什么?

state 对象的树形结构必定会随着 web 应用的复杂性而变得越来越深。当某个 action.type 所对应的 case 只是要修改 state.a.b.c.d.e.f 这个属性时,我的 handleCase 函数写起来就非常难看,我必须在这个函数的头部验证 state 对象有没有那个属性。

我们需要这种模式:

//这个 reducer 的 state 属性不是全局 state 本身
//而是它的一个子代属性,比如 state.todos 这个对象
//返回的 new state 也会合并到 state.todos 属性中
export default (state, action) => {
    switch (action.type) {...}
}

如上所示,写起来是普通的 reducer ,但拿到的不是全局 state

实现方法很简单,遍历一个「全是方法」的「函数储存对象」,返回新对象,这个新对象的 key 跟「函数储存对象」一样,它的 value 则是「函数储存对象」的同名方法接受 (state[key], action) 参数的返回值。

var reducers = {
    todos: (state, action) { //预期此处的 state 参数是全局 state.todos 属性
        switch (action.type) {...} //返回的 new state 更新到全局 state.todos 属性中
    },
    activeFilter: (state, action) { //预期拿到 state.activeFilter 作为此处的 state
        switch (action.type) {...} //new state 更新到全局 state.activeFilter 属性中
    }
}

//返回一个 rootReducer 函数
//在内部将 reducers.todos 函数的返回值,挂到 state.todos 中
//在内部将 reducers.activeFilter 函数的返回值,挂到 state.activeFilter 中
var rootReducer = combineReducers(reducers)

redux 的 combineReducers 源码如下:

//combination 函数是 combineReducers(reducers) 的返回值,它是真正的 rootReducer
//finalReducers 是 combineReducers(reducers) 的 reducers 对象去掉非函数属性的产物
 //mapValue 把 finalReducers 对象里的函数,映射到相同 key 值的新对象中
function combination(state = defaultState, action) {
    var finalState = mapValues(finalReducers, (reducer, key) => {
      var newState = reducer(state[key], action); //这里调用子 reducer 
      if (typeof newState === 'undefined') {
        throw new Error(getErrorMessage(key, action));
      }
      return newState; //返回新的子 state
    });
    //...省略一些业务无关的代码
    return finalState; //返回新 state
 };

相信你也注意到了,mapValue 只是一级深度的映射,目前 redux 并没有提供简便的映射到 state.a.b 一级以上深度的 state 的方法。这是它目前的不足之处。

设想我们做一个移动端 webapp,有很多个 view 在单页中,比如 index 页,list 页,detail 页,redux 提供的「一级分解」一下子就让各个 view 消耗完了,如果还要分割每个 view 的 state ,看起来会很麻烦。

在这里提供几个解决思路:

第一个方案是 superGetter/superSetter

export default (state, action) => {
    switch (action.type) {
        case A:
        let subState = superGetter(state, 'a.b.c') //根据 path 深度获取属性值
        return superSetter(state, 'a.b.c', handleA(subState)) //根据 path 深度设置属性
        default:
        state
    }
}

第二个方案是嵌套 combineReducers

var todosReducers = {
    active: (state, action) => { //拿到全局 state.todos.active
        switch (action.type) {
            case A: //处理 A 场景
            return handleA(state)
            case B: //处理 B 场景
            return handleB(state)
            default:
            return state
        }
    },
    completed: (state, action) => { //拿到全局 state.todos.completed
        switch (action.type) {
            case C: //处理 C 场景
            return handleC(state)
            default:
            return state
        }
    }
}

var todosRootReducer = combineReducers(todosReducers)

var reducers = {
    todos: (state, action) => { //拿到全局 state.todos
        switch (action.type) {
            case A:
            case B:
            case C:
            // A B C 场景都传递给 todosRootReducer
            return todosRootReducer(state, action)
            case D:
            //...handle state
            default:
            return state
        }
    }
}

//rootReducer(state, action) 这里的 state 是真正的全局 state
var rootReducer = combineReducers(reducers)

需要注意的是,reduxcombineReducers(reducers) 的返回值 rootReducers, 总是返回新的 state,它不是修改旧 state,而是创建空对象,然后将 key/value 往上面挂载。只有在 reducers 对象上的 key 才会被迁移。也就是说:

var rootReducers = combineReducers({
    a() {
        //TODO
    },
    b() {
        //TODO
    },
    c() {
        //TODO
    }
})

// newState 只有 a/b/c 三个属性,没有 d 属性,因为 reducers 对象只有 a/b/c
var newState = rootReducers({
    a:1,
    b:2,
    c:3,
    d: 4
}, {
    type: 'TEST'
})

第三个方案更为激进,目前 redux 没有提供,需要修改其源码。这个模式是 transformer (转换器)

//combineReducers 新增第二个参数 transformers
export default combineReducers(reducers, transformers) {
    //..一些预处理工作
    return function combination(state = defaultState, action) {
        var finalState = mapValues(finalReducers, (reducer, key) => {
            var transformer = transformers[action.type] //根据 action.type 来筛选
            var newState
            if (typeof transformer === 'function') {//如果有转换器
                //控制权交给转换器
                newState = transformer(state[key], action, reducer, key)
            } else {
                //否则采取默认模式
                newState = reducer(state[key], action);
            }
            if (typeof newState === 'undefined') {
                throw new Error(getErrorMessage(key, action));
            }
            return newState;
        });
        return finalState
    }
}

有了上面的修改,我们就可以针对 action.type 来选择全局 state 的更新路径了。

var transformers = {
    'ACTION_TYPE1': (state, action, reducer) => {
        return {
            ...state,
            newProp: reducer(state.prop, action) //更新到 newProp 属性中去
        }
    },
    'ACTION_TYPE2': (state, action, reducer) => {
        return {
            ...state,
            otherProp: reducer(state.otherProp, action) //更新到 otherProp 属性中去
        }
    }
}

var rootReducers = combineReducers(reducers, transformers)

如上所示,有了 transformers,不必再嵌套 combineReducers。不过,换个角度看,这个模式只是将原本要在 handleA(state) 里要做的属性查询工作,搬到了 transfromer 中,让 handleA 可以直接处理它需要的 state 对象而已。

总的而言,combineReducers 不是一个必需品,它只是用来分发全局对象的属性到各个 reducer 中去,如果你觉得它太绕,你可以选择直接在每个 handleCase 函数中查询 state 属性,合成 newState 并返回即可。这时候,你只需要一个 reducers 函数,它的 switch 语句处理所有可能的 action.type;想想就是一个超长的函数。

为什么要有 createStore ?

既然 redux 建议只维护一个全局 state ,为什么要搞一个 createStore 函数呢?直接创建一个空对象,然后缓存起来,不断投入到 reducer(state, action) 更新状态不就行了?

这会儿该说到「函数式编程」里的几个概念了。「无副作用函数」与「不变值」。

上面提到的 action 跟 reducer 函数,都是普通的纯函数。对于 action 函数 来说,输入相同的参数无限次,它的返回值也相同。而有了「不变值」,我们得到的好处是,在 react component 的 shouldComponentUpdate(nextProps, nextState) 里,可以直接拿当前 props 跟 nextProps 做 === 对比,如果相等,说明不用更新,如果不相等,则更新到视图。

如果不是返回新 state,只是修改旧 state,我们就很难做到「回退/撤销」以及跟踪全局状态。对比两个数据是否同一,也无法用 ===,而得用 deepEqual 深度遍历来对比值,很耗费性能。

另外, 上面提到的 action 函数,它只是返回一个 plain object 而已,除此之外,它什么也没做。是谁把它传递到 reducers(state, action) 调用?

reducers|state|action 这三个东西由谁来协调?

此时,createStore(reducer, initialState) 呼之欲出;它接受一个 reducer 函数跟 initialState 初始化的全局状态对象,返回几个「公共方法」:dispatch|getState|subscribe。这里我只列举了对我们有重要意义的三个,还剩两个不太重要,可自行参考 redux 文档。

createStore 做的事情在《Javascript 高级程序设计》一书里有讲解,很简明易懂。

//此处为示意,不是 redux 的源码本身
export default createStore(reducer, initialState) {
    //闭包私有变量 
    let currentState = initialState
    let currentReducer = reducer
    let listeners = []

    //返回一个包含可访问闭包变量的公有方法
    return {
        getState() {
            return currentState //返回当前 state
        },
        subscribe(listener) {
            let index = listeners.length
            listeners.push(listener) //缓存 listener
            return () => listeners.splice(i, 1) //返回删除该 listener 的函数
        },
        dispatch(action) {
            //更新 currentState
            currentState = currentReducer(currentState, action)
            listeners.slice().forEach(listener => listener())
            return action //返回 action 对象
        }
    }
}

如上所示,redux 返璞归真的核心代码,没有什么原型继承、面向对象这类绕来绕去的事物。

createStore 的返回值是一个对象,通常我们保存在 store 这个变量名里。其实 store 是一个只有方法,没有数据属性的对象,用 JSON.stringify 去系列化它,得到的是空对象。真正的 state 包含在闭包中,通过公有方法 getState 来获取。

dispatch 方法,是 store 对象提供的更改 currentState 这个闭包变量的唯一建议途径。注意,我是说唯一建议,不是说唯一途径,因为 getSate 拿到的是 currentState 的对象引用,我们还是可以在外头改动它,虽然不建议。

subscribe 方法是一个简单的事件侦听方法,在 dispatch 里更新完 currentState 后调用,不管是什么 action 触发的更新他,它都会调用,并且没有任何参数,只是告诉你 state 更新了。这个方法在后面的提到的服务端同构应用之「镜像 store 」中有妙用。

至此, createStorestore 的全部重要内容都揭示了,它们就是如此简洁。

为什么要有 bindActionCreators ?

通过 createStore 我们拿到了 store, 通过 store.dispatch(action) 我们可以免去手动调用 reducer 的负担,只处理 action 就可以了,一切都很方便。只是,有两种意义上的 action,一种是 action 函数,另一种是 action 对象,action 函数接受参数并返回一个 action 对象。

action 函数是工厂模式,专门生产 action 对象。所以我们可以通过重新命名,更清晰的区别两者,action 函数就叫 actionCreator,它的返回值叫 action

store.dispatch(action) 这里的 action 是一个对象,不是函数,它是 actionCreator 返回的,所以实际上要这样调用 store.dispatch(actionCreator(...args)),很麻烦是吧?

原本的 reducer(state, action) 模式,我们用 createStore(reducer, initialState) 转换成 store.dispatch(action),现在发现还不够,怎么做?再封装一层呗,这就是函数式思想的体现,通过反复组合,将多参数模式,转化为单参数模式。

怎么组合?

对于单个 actionCreator ,我们可以轻易地 bindActionCreator

//将 actionCreator 跟 dispatch 绑定在一起
let bindActionCreator => (actionCreator, dispatch) {
  return (...args) => dispatch(actionCreator(...args));
}

//普通工厂函数,返回一个对象
let addItem = text => ({
    type: 'ADD_ITEM',
    text
})

//跟 store.dispatch 绑定起来,成为真正可以改变 currentState 的 action 函数
let addItem = bindActionCreator(addItem, store.dispatch)

对于多个 actionCreator,我们可以像 reducers 一样,组织成一个 key/action 的组合嘛。

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') { //如果是单个 actionCreator,绑定一词
    return bindActionCreator(actionCreators, dispatch);
  }
  //返回一个改造过的「函数组合」
  return mapValues(actionCreators, actionCreator =>
    bindActionCreator(actionCreator, dispatch)
  )

如上所示,我们用 bindActionCreators 得到了真正具有改变全局 state 能力的许多函数,剩下的事情,就是将这些函数分发到各个地方,由各个 event 自主调用即可(正如在「为什么需要 action ?」 一节里介绍的)。

redux 工作流程是怎样的?

至此,我们来梳理一下,actionCreator|reducer|combineReducers|createStore|bindActionCreators 这些函数的书写与组合的过程以及顺序。

首先,我们要先设计一些「常量」,因为 action.type 通常是字符串常量。为了便于集中管理,以及利于压缩代码,我们最好将常量放在单独的文件夹里,根据类型的不同放置在不同的文件中。

以 [Isomorphism-react-todomvc] 为例,constants (中译:常量)文件夹里有如下文件:

//ActionTypes.js 真正改动了数据的 actionType 在这里
export const ADD_ITEM = 'ADD_ITEM'
export const DELETE_ITEM = 'DELETE_ITEM'
export const DELETE_ITEMS = 'DELETE_ITEMS'
export const UPDATE_ITEM = 'UPDATE_ITEM'
export const UPDATE_ITEMS = 'UPDATE_ITEMS'

//API.js 服务端接口统一放这里
export const API_TODOS = '/todos'

//SocketTypes.js websocket 也触发了某个 action 改变了 state,单独放这里
export const SERVER_UPDATE = 'SERVER_UPDATE'

//KeyCode.js 键盘的回车键与取消键对应的编码
export const ENTER_KEY = 13
export const ESCAPE_KEY = 27

//FilterTypes.js 只是筛选数据,没有改变 state 的过滤 action 的常量
export const FILTER_ITEMS = 'FILTER_ITEMS'
export const SHOW_ALL = 'SHOW_ALL'
export const SHOW_ACTIVE = 'SHOW_ACTIVE'
export const SHOW_COMPLETED = 'SHOW_COMPLETED'

我们的「常量设计」,可以清晰地反应我们整个 web 应用的业务架构设计;这方面没弄好,随着应用的复杂性增加,会越来越难以维护。当然,比设计常量更靠前的是,设计整个应用的 state 树的结构,这方面不同业务有不同的设计思路,这里无法多做介绍。

由于 todomvc 的业务逻辑很简单,所以它的 state 设计是这样的:

let state = {
    todos: [{
        id: 123,
        text: 'todo item',
        status: false
    }],
    activeFilter: SHOW_ALL
}

有了常量,我们就可以写 actionCreator 了,它们被放置在 actions 文件夹里。

//index.js
import * as types from '../constants/ActionTypes'

export function addItem(text) {
    return { type: types.ADD_ITEM, text }
}

export function deleteItem(id) {
    return { type: types.DELETE_ITEM, id }
}

export function updateItem(data) {
    return { type: types.UPDATE_ITEM, data }
}

export function deleteItems(query) {
    return { type: types.DELETE_ITEMS, query }
}

export function updateItems(data) {
    return { type: types.UPDATE_ITEMS, data }
}

action 是预处理,下一个环节是再处理函数 reducer,它们被放置在 reducers 文件夹里。

//todos.js
import { ADD_ITEM, DELETE_ITEM, UPDATE_ITEM, DELETE_ITEMS, UPDATE_ITEMS } from '../constants/ActionTypes'
import { SERVER_UPDATE } from '../constants/SocketTypes'

export default (state = [], action) => {
    switch (action.type) {
        case ADD_ITEM: //添加 item,放在数组第一个位置
            return [createItem(action.text), ...state]
        case DELETE_ITEM: //删除 item 就是根据 id 过滤掉
            return state.filter(item => item.id !== action.id)
        case UPDATE_ITEM: //更新item 由 updateItem helper 函数执行
            return updateItem(action.data, state)
        case UPDATE_ITEMS: //更新所有 item,就是每个就合并 action.data
            return state.map(item => Object.assign({}, item, action.data))
        case DELETE_ITEMS: //删除 item,过滤掉符合 action.query 对象描述的 item
            return filterItems(action.query, state)
        case SERVER_UPDATE: //服务端推送 action,整个替换掉 todos
            return action.state.todos
        default: //其他没匹配到的 action,返回原 state
            return state
    }
}

//filter.js
import { FILTER_ITEMS, SHOW_ALL, SHOW_ACTIVE, SHOW_COMPLETED } from '../constants/FilterTypes'

let hashToFilter = {
    '#/': SHOW_ALL,
    '#/active': SHOW_ACTIVE,
    '#/completed': SHOW_COMPLETED
}

export default (state = SHOW_ALL, action) => {
    switch (action.type) {
        case FILTER_ITEMS: //单纯的模式匹配,默认显示 SHOW_ALL
        return hashToFilter[action.active] || SHOW_ALL
        default:
        return state
    }
}

如上所示,todos.js 负责处理 state.todos 属性,filter.js 负责处理 state.activeFilter 属性,所以我们需要用 combineReducers 将它们组织起来。

// reducers/index.js
import { combineReducers } from 'redux'
import list from './list'
import filter from './filter'
//只需要用到一级分解,真是万幸呢
export default combineReducers({
    todos: list,
    activeFilter: filter
})

目前我们有了 actionCreators(就是在 actions 文件夹下的 index.js 的模块输出) 以及 rootReducer 函数(就是上面reducers/index.js的模块输出),接下来,就是用 createStorerootReducer 给吞掉。

// ./store/index.js
import { createStore } from 'redux'
import rootReducers from '../reducers'
export default initialState => {
    return createStore(rootReducers, initialState)
}

我们调用 createStore 拿到 store 之后,就拿到了 store.dispatch,然后用 bindActionCreatorsactionCreators 对象跟 dispatch,粘合在一起。

let dispatchToProps = dispatch => bindActionCreators(actions, dispatch)
//分发给 component,让它从上到下不断分发 action
<View {...dispatchToProps(store.dispatch)} {...props} />

如果你用 react-redux ,你就用它提供的 <Provider></Provider + connect 组织单项数据流。

如果你只用 redux,你可以封装一个 render 函数,在 store.subscribe 事件回调里使用。如下所示:

//app.js
let store = createStore(reducers, initialState)
let actions = bindActionCreators(actionCreators, store.dispatch)
let render = () => {
    React.render(
        <Root {...store.getState()} {...actions} >, //传 action,传 state 数据
        document.getElementById('container')
    )
}

store.subscribe(render) //当 state 变化时,重新渲染

如上所示,组织 redux 的流程莫过于:

  • 设计全局 state 的数据结构
  • 设计更改 state 数据的 actionTypes 常量以及其他跟视图展现相关的 actionTypes 常量
  • 根据 actionTypes 常量,书写 actionCreator 。
  • 根据各个 actionCreator 的返回值,涉及 reducer 做数据的最后处理
  • 在有了 reducer 函数之后,createStore(reducer, initState) 得到 store 对象
  • 用 bindActionCreators 函数将 actionCreators 和 store.dispatch 绑定起来,得到一组能修改全局状态的函数
  • 分发各个状态修改函数到各个 DOM 事件中。

为什么需要 applyMiddlewares ?

reducer(state, action) 这个调用方式所反映的 reduceraction 的关系很近,action 就是 reducer 的第二个参数嘛。然而,上面所示的 redux 流程上看,它们却隔着 createStore|store.dispatch|bindActionCreators 三个 API ,才最后汇集到一处。

当我们失去对 reducer 的直接控制权之后,这意味着我们的调试不方便了。原本我们可以像下面那样做:

//我们可以这样:
cosnole.log(state, action) //调用之前
state = reducer(state, action)
cosnole.log(state, action) //调用之后

//虽然现在我们可以这样代替,但这里 action 是我们构造的
//dom 事件里触发的 action,被隐藏得很深,也无法从 store.subscribe 里侦听到,它不传参数
cosnole.log(store.getState(), action) //调用之前
store.dipatch(action)
cosnole.log(store.getState(), action) //调用之后

就算只是为了调试代码,打印出 action 日志,我们也值得设计解决方案。applyMiddlewares 就是一个有用的思路。它的原理很简单,在《JavaScript 高级程序设计》里也有提到,就是模块模式。

export default function applyMiddleware(...middlewares) {
  return createStore => (reducer, initialState) => {
    var store = createStore(reducer, initialState);
    var dispatch = store.dispatch; //拿到真正的 dispatch
    //将最重要的两个方法 getState/dispatch 整合出来
    var middlewareAPI = {
      getState: store.getState,
      dispatch: action => dispatch(action)
    };
    //依次传递给 middleware,让它们有控制权
    var chain = middlewares.map(middleware => middleware(middlewareAPI));
    dispatch = compose(...chain, dispatch); // 再组合出新的 dispatch

    //返回新的 store 对象,其 dispatch 方法已经被传递了很多层
    //每一层都可以调用 dispatch,也可以调用 next 让下一层考虑调用 dipatch
    //最后一个 next 就是 store.dispatch 本身。
    return {
      ...store,
      dispatch
    };
  };
}

然后我们可以这样写中间件了。

//redux-thunk
export default function thunkMiddleware({ dispatch, getState }) {
  return next => action =>
    typeof action === 'function' ? // action 居然是函数而不是 plain object?
      action(dispatch, getState) : //在中间件里消化掉,让该函数控制 dispatch 时机
      next(action); //否则调用 next 让其他中间件处理其他类型的 action
}

注意,在每个中间件里存在两个 dispatch 功能。一个是 { dispatch, getState },这是在 middlewareAPI对象里的 dispatch 方法,另一个是 next,它是 chain 链条的最后一环 dispatch = compose(...chain, dispatch)

如果你不想在将 action 传递到在你之后的中间件里,你应该直接显式地调用 dispatch,不要调用 next。如果你发现这个 action 对象不包含你感兴趣的数据,是你要忽略的 action,这时应该传给 next,它可能是其他中间件的处理目标。

redux 的中间件模式,将 dispatch 的步骤拉长并且细化,使得我们可以处理更多类型的 action,比如带函数的,比如带 promise 的等等,我们可以在真正的 store.dispatch 调用之前,先把看似不合格的 action 对象消化掉,吐出 store.dispatch 能直接调用的数据结构即可。

除了这个 rerdux-thunk (上面的示例真是它的源码,不信请点击这里)中间件之外,还可以写很多不同类型的,其中 redux-logger 就是一开始说的对调试 redux 代码很必要的中间件。

redux 服务端渲染怎么处理?

如果你现在(2015.08.24)去 redux 的官方文档里查阅,你会发现,server rendering 这一块还是不可点击的空白状态。然而,我们既然已经深入到源码层次,自己找出一条途径,也是可以的(只适用于 node.js)。

首先,一开始我们就说过,redux 是无依赖的,所以它可以直接用在 node.js 运行时里。关键在于 react-redux 的服务端渲染方式,它所提供的 connectProvider 组合,扰乱了我们对 react component 的掌控与认知。

我目前的做法是,将跟 redux 有关的所谓的 smart component (智能组件),放到 containers 文件夹里,普通的 react component,放在 components 文件夹里,在客户端时,我们渲染 containers 里的 redux 组件。而在服务端,我们渲染 conponents 的普通组件,redux 组件要穿的参数,我们一一构造出来即可。

//server side
let store = createStore(rootReducers, { todos: [] })

store.getComponent = () => {
    let props = stateToProps(store.getState())
    let actions = dispatchToProps(() => {}) //构造空函数给 actions,反正没有 dom 事件
    return React.renderToString(<View {...props} {...actions} />)
}

具体实现可以参考 Isomorphism-react-todomvc 项目。

镜像 store 模式

这里介绍的所谓镜像 store 模式,并非 redux 官方文档里提到的,而是在实践过程中我所发现的有趣用法,大家看看就好,仅供参考,不要误以为是官方推荐模式即可。

思路很简单,既然每个 actionCreator 返回的都是 plain javascript object,它们都是可以被 JSON.stirngify 系列化的。也就是说可以 post 到服务端,如果服务端也有一个同样的 store,它 store.dispatch 一下,不就跟客户端一致了?

这样的话,我们只需要传更轻量的 action 数据,这种做法犹如 graphql 一般。另外,在服务端的 store.subscribe 中我们绑定一个 websocket.emit 函数,就可以把服务端根据 action 所做的数据更新同步到所有浏览器端了。

//store
import { createStore } from 'redux'
import rootReducers from '../public/js/src/index/reducers'
let store = createStore(rootReducers, { todos: [] })
export default store

//router
router.post('/todos', (req, res) => {
    store.dispatch(req.body) //直接 dispatch action 更新 state
    res.json(Object.assign({}, ok, {
        data: req.body
    }))
})

// ./bin/www
let server = require('http').createServer(app);
let io = socketIO(server)
store.subscribe(() => io.emit('change', store.getState())) //服务端推送

// ./index.js
//浏览器端响应一个 dispatch
io().on('change', state => store.dispatch({
    type: SERVER_UPDATE,
    state
}))

// .//reducers/list.js
export default (state = [], action) => {
    switch (action.type) {
        //...other case
        case SERVER_UPDATE:
        return action.state.todos //将整个 todos 数据跟服务端同步起来
        default:
        return state
    }
}

// ./middleware/restful.js
import { API_TODOS } from '../constants/API'
import * as ActionTypes from '../constants/ActionTypes'

export default store => next => action => {
    if (action.type in ActionTypes) { //用中间件模式,筛选有修改数据作用的 action
        fetch(API_TODOS, {
            method: 'post',
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(action) //打包发送到服务端
        })
    }
    //对 action 什么都不做,让浏览器端 action 继续传递
    return next(action) //可以不用等待服务端就更新视图
}

结尾

这篇文章写得长了,就此结尾罢。

总体而言,redux 是一个优秀的新技术,厉害到自己开辟新生态,不容小觑。它也有一些缺陷,比如不容易处理 state 的深度 path 路径问题,比如分发太多 action 到 react component 时,一层层验证 propTypes 的繁琐(虽然别的 flux 实现也有这个问题)等。

比起其他 flux 模式, redux 已然优越。推荐使用。

登录后回复,如无账号,请使用邀请码注册