http://gaearon.github.io/redux/index.html ,文档在 http://rackt.github.io/redux/index.html 。本文不是官方文档的翻译。你可以在阅读官方文档之前和之后阅读本文,以加深其中的重点概念。
根据该项目源码的习惯,示例都是基于 ES2015 的语法来写的。
Redux 是应用状态管理服务。虽然本身受到了 Flux 很深的影响,但是其核心概念却非常简单,就是 Map/Reduce 中的 Reduce。
我们看一下 Javascript 中 Array.prototype.reduce
的用法:
const initState = '';
const actions = ['a', 'b', 'c'];
const newState = actions.reduce(
( (prevState, action) => prevState + action ),
initState
);
从 Redux 的角度来看,应用程序的状态类似于上面函数中的 initState
和 newState
。给定 initState
之后,随着 action
的值不断传入给计算函数,得到新的 newState
。
这个计算函数被称之为 Reducer
,就是上例中的 (prevState, action) => prevState + action
。
Immutable State
Redux 认为,一个应用程序中,所有应用模块之间需要共享访问的数据,都应该放在 State
对象中。这个应用模块可能是指 React Components,也可能是你自己访问 AJAX API 的代理模块,具体是什么并没有一定的限制。State
以 “树形” 的方式保存应用程序的不同部分的数据。这些数据可能来自于网络调用、本地数据库查询、甚至包括当前某个 UI 组件的临时执行状态(只要是需要被不同模块访问)、甚至当前窗口大小等。
Redux 没有规定用什么方式来保存 State
,可能是 Javascript 对象,或者是 Immutable.js 的数据结构。但是有一点,你最好确保 State 中每个节点都是 Immutable 的,这样将确保 State 的消费者在判断数据是否变化时,只要简单地进行引用比较即可,例如:
newState.todos === prevState.todos
从而避免 Deep Equal 的遍历过程。
为了确保这一点,在你的 Reducer
中更新 State
成员需要这样做:
`let myStuff = [
{name: 'henrik'}
]
myStuff = [...mystuff, {name: 'js lovin fool']`
myStuff
是一个全新的对象。
如果更新的是 Object ,则:
let counters = {
faves: 0,
forward: 20,
}
// this creates a brand new copy overwriting just that key
counters = {...counters, faves: counters.faves + 1}
而不是:
counters.faves = counters.faves + 1}
要避免对 Object 的 in-place editing。数组也是一样:
let todos = [
{ id: 1, text: 'have lunch'}
]
todos = [...todos, { id: 2, text: 'buy a cup of coffee'} ]
而不是:
let todos = [
{ id: 1, text: 'have lunch'}
]
todos.push({ id: 2, text: 'buy a cup of coffee'});
遵循这样的方式,无需 Immutable.js 你也可以让自己的应用程序状态是 Immutable 的。
在 Redux 中,State
只能通过 action
来变更。Reducer
就是根据 action
的语义来完成 State
变更的函数。Reducer
的执行是同步的。在给定 initState
以及一系列的 actions
,无论在什么时间,重复执行多少次 Reducer
,都应该得到相同的 newState
。这使得你的应用程序的状态是可以被 Log 以及 Replay 的。这种确定性,降低了前端开发所面临的复杂状态的乱入问题。确定的状态、再加上 Hot-Reloaidng 和相应的 Dev-Tool,使得前端应用的可控性大大增强了。
State 结构设计
Redux (Flux) 都建议在保存 State
数据的时候,应该尽可能地遵循范式,避免嵌套数据结构。如果出现了嵌套的对象,那么尽量通过 ID 来引用。
假设远程服务返回的数据是这样的:
[{
id: 1,
title: 'Some Article',
author: {
id: 1,
name: 'Dan'
}
}, {
id: 2,
title: 'Other Article',
author: {
id: 1,
name: 'Dan'
}
}]
那么,转换成以下形式会更有效率:
{
result: [1, 2],
entities: {
articles: {
1: {
id: 1,
title: 'Some Article',
author: 1
},
2: {
id: 2,
title: 'Other Article',
author: 1
}
},
users: {
1: {
id: 1,
name: 'Dan'
}
}
}
}
范式化的存储让你的数据的一致性更好,上例中,如果更新了users[1].name
,那么在显示 articles
的 component 中,作者姓名也被更新了。
其实传统关系数据库的设计原则就是如此,只不过随着对数据分布能力和水平扩展性的要求(放弃了一定程度的数据一致性),服务端数据的冗余越来越多。但是回到客户端,由于需要保存的数据总量不大(往往就是用户最近访问数据的缓存),也没有分布式的要求,因此范式化的数据存储就更有优势了。除了可以收获一致性,还可以减少存储空间(存储空间在客户端更加宝贵)。
除此之外,范式化的存储也利于后面讲到的 Reducer
局部化,便于将大的 Reducer
分割为一系列小的 Reducers
。
由于服务器端返回的 JSON 数据(现在常见的方式)往往是冗余而非范式的,因此,可能需要一些工具来帮助你转换,例如:https://github.com/gaearon/normalizr , 虽然很多时候自己控制会更有效一些。
Reducer
下面我们以熟悉 todoApp
来看一下 Reducer
的工作方式:
function todoAppReducer(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
}]
});
default:
return state;
}
}
这个例子演示了 Reducers
是如何根据传入的 action.type
分别更新不同的 State
字段。
如果当应用程序中存在很多 action.type
的时候,通过一个 Reducer
和巨型 switch
显然会产生难以维护的代码。此时,比较好的方法就是通过组合小的 Reducer
来产生大的 Reducer
,而每个小 Reducer
只负责处理 State
的一部分字段。如下例:
import { combineReducers } from 'redux';
const todoAppReducer = combineReducers({
visibilityFilter: visibilityFilterReducer
todos: todosReducer
});
visibilityFilterReducer
和 todosReducer
是两个小 Reducers
,其中一个如下:
function visibilityFilterReducer(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}
visibilityFilterReducer
仅仅负责处理 State.visibilityFilter
字段的状态(通过 action.type
为 SET_VISIBILITY_FILTER
的 action 来改变)。Reducers
划分是通过向 combineReducers
传递如下形式的参数实现的:
{
field1: reducerForField1,
field2: reducerForField2
}
filed1
和 filed2
表示 State
中的字段,reducerForField1
和 reducerForField2
是对应的 Reducers
,每个 Reducers
将仅仅获得 State.field1
或者 state.field2
的值,而看不到 State
下的其他字段的内容。响应的返回结果也会被合并到对应的 State
字段中。每个 Reducer
如果遇到自己不能处理的 action
,那么必须原样返回传入的 state
,或者该 Reducer
设定的初始状态(如果传入的 state
是 undefined
)。
使用 combineReducers
的前提是,每一个被组合的 Reducer
仅仅和 State
的一部分数据相关,例如:todos Reducer
只消费 State.todos
数据,也只产生 State.todos
数据。这个基本的原则和上面提到的“State 结构设计”范式相结合,可以满足我们大部分需求。
不过,有时我们就是需要在一个 Reducer
之中访问另外一个 Reducer
负责的 state
,这需要我们创建更上一层的 Reducer
(Root Reducer) 来控制这个过程,例如:
function a(state, action) { }
function b(state, action, a) { } // depends on a's state
function something(state = {}, action) {
let a = a(state.a, action);
let b = b(state.b, action, a); // note: b depends on a for computation
return { a, b };
}
在这个例子中,我们有两个 Reducers
, a
和 b
,其中,b
在计算自己的 state
的还需要依赖 a
的计算结果。因此,我们就不能依靠 combineReducers
来完成这种需求,而是需要自己写 Root Reducer 了。reduce-reducers 也可以帮我们完成类似的任务:
var reducers = reduceReducers(
combineReducers({
router: routerReducer,
customers,
stats,
dates,
filters,
ui
}),
// cross-cutting concerns because here `state` is the whole state tree
(state, action) => {
switch (action.type) {
case 'SOME_ACTION':
const customers = state.customers;
const filters = state.filters;
// ... do stuff
}
}
);
上面的例子里,在 combineReducers
的基础上,如果某些 action
需要触发跨 Reducers
的状态改变,则可以用上面的写法。reduce-reducers 组合(每个参数就是一个 Reducer
)的每一个 Reducer
都可以获取整个 State
,所以请不要滥用(请参见相关讨论:https://github.com/reactjs/redux/issues/749 ),在大部分情况下,如果严格遵循数据范式,通过计算的方法获得跨越 Reducers
的状态是推荐的方法(http://redux.js.org/docs/recipes/ComputingDerivedData.html )。
一个 Reducer
可以处理多种 action.type
,而 一种 action.type
也可能被多个 Reducers
处理,这是多对多的关系。以下 Helper 函数可以简化 Reducer
的创建过程:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
export const todosReducer = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
}
Store
在 Redux 中,Store
对象就是用来维护应用程序状态的对象。构造 Store
对象,仅需要提供一个 Reducer 函数即可。如前所述,这个 Reducer
函数是负责解释 Action
对象的语义,从而改变其内部状态(也就是应用程序的状态)。
因此 Store 对象有两个主要方法,一个次要方法:
-
store.getState()
: 获取最近的内部状态对象。 -
store.dispatch(action)
: 将一个action
对象发送给reducer
。
一个次要方法为:const unsure = store.subscribe(listener)
,用来订阅状态的变化。在 React + Redux 的程序中,并不推荐使用 store.subscribe
。但是如果你的应用程序是基于 Observable 模式的,则可以用这个方法来进行适配;例如,你可以通过这个方法将 Redux 和你的 FRP (Functional Reactive Programming) 应用结合。
下面这个例子演示了 Store
是如何建立的:
import { combineReducers, createStore } from 'redux';
import * as reducers from './reducers';
const todoAppReducer = combineReducers(reducers);
const store = createStore(todoAppReducer); // Line 5
store.dispatch({type: 'ADD_TODO', text: 'Build Redux app'});
我们也可以在 createStore
的时候为 Store
指定一个初始状态,例如替换第 5 行为:
const store = createStore(reducers, window.STATE_FROM_SERVER);
这个例子中,初始状态来自于保存在浏览器 window
对象的 STATE_FROM_SERVER
属性。这个属性可不是浏览器内置属性,是我们的 Web Server 在返回的页面文件中以内联 JavaScript 方式嵌入的。这是一种 Universal(Isomorphic) Application 的实现方式。Client 无需发起第一个 AJAX API 请求,就可以直接从当前页面中直接获得初始状态。
Action
在 Redux 中,改变 State
只能通过 action
。并且,每一个 action
都必须是 Javascript Plain Object,例如:
{
type: 'ADD_TODO',
text: 'Build Redux app'
}
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
如果你用过 Express,那么就会熟悉它的 Middleware 系统。在 HTTP Request 到 Response 处理过程中,一系列的 Express Middlewares 起着不同的作用,有的 Middleware 负责记录 Log,有的负责转换内部异常为特定的 HTTP Status 返回值,有的负责将 Query String 转变到 request
对象的特定属性。
Redux Middleware 的设计动机确实是来自于 Express 。其主要机制为,建立一个 store.dispatch
的链条,每个 middleware 是链条中的一个环节,传入的 action 对象逐步处理,直到最后吐出来是 Javascript Plain Object。先来看一个例子:
import { createStore, combineReducers, applyMiddleware } from 'redux';
// applyMiddleware takes createStore() and returns// a function with a compatible API.
let createStoreWithMiddleware = applyMiddleware(
logger,
crashReporter
)(createStore);
// Use it like you would use createStore()let todoApp = combineReducers(reducers);
let store = createStoreWithMiddleware(todoApp);
这个例子中,logger
和 crashReporter
这两个 Middlewares 分别完成记录 action
日志和记录 action
处理异常的功能。
logger
的代码如下:
// Logs all actions and states after they are dispatched.
const logger = { getState } => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', getState());
return result;
};
logger
是一个 currying (这是函数式编程的一个基本概念,相比 Flux,Redux 大量使用了函数式编程的范式)之后的函数。next
则是下一个 Middleware 返回的 dispatch
函数(后面会有分析)。对于一个 Middleware 来说,有了 store
对象,就可以通过 store.getState()
来获取最近的应用状态以供决策,有了 next
,则可以控制传递的流程。
ES6 的 Fat Arrow Function 语法(logger = store => next => action =>
)让原本 function
返回 function
的语法变得更简洁(I love ☕️script!)。
工业化的 logger
实现可以参见:https://github.com/fcomb/redux-logger 和 https://github.com/fcomb/redux-diff-logger 。同一个作者写了两个,后面这个支持 State
的差异显示。
vanilla promise
Middleware 还可以用来对传入的 action
进行转换,下面这个例子里,传入的 action
是一个 Promise(显然不符合 action
必须是 Javascript Plain Object 的要求),因此需要进行转换:
/**
* Lets you dispatch promises in addition to actions.
* If the promise is resolved, its result will be dispatched as an action.
* The promise is returned from `dispatch` so the caller may handle rejection.
*/
const vanillaPromise = { getState, dispatch } => next => action => {
if (typeof action.then !== 'function') {
return next(action);
}
// the action is a promise, we should resolve it first
return Promise.resolve(action).then(dispatch);
};
这个例子中,如果传入的 action
是一个 Promise
(即包含 .then 函数,这只是一个粗略的判断),那么就执行这个 Promise
,当 Promise
执行成功后,将结果直接传递给 store.dispatch
(这个例子中我们短路了 Middlewares 链中的后续环节)。当然,我们要确保 Promise
的执行结果返回的是 Javascript Plain Object。
这种用法可能并非常用,但是从这个例子我们可以体会到,我们可以定义自己 action
的语义,然后通过相应的 middleware 进行解析,产生特定的执行逻辑以生成最终的 action
对象。这个执行过程可能是同步的,也可能是异步的。
从这个例子你可能也会发现,如果们也装载了 logger
Middleware,那么 logger
可以知道 Promise action
进入了 dispatch
函数链条,但是却没有机会知道最终 Promise
执行成功/失败后发生的事情,因为无论 Promise
执行成功与否,都会直接调用最原始的 store.dispatch
,没有走 Middlewares 创建的 dispatch
函数链条。
对 Promise 的完整支持请参见:https://github.com/acdlite/redux-promise 。
Scheduled Dispatch
下面这个例子略微复杂一些,演示了如何延迟执行一个 action
的 dispatch
。
/**
* Schedules actions with { meta: { delay: N } } to be delayed by N milliseconds.
* Makes `dispatch` return a function to cancel the interval in this case.
*/
const timeoutScheduler = store => next => action => {
if (!action.meta || !action.meta.delay) {
return next(action);
}
let intervalId = setTimeout(
() => next(action),
action.meta.delay
);
return function cancel() {
clearInterval(intervalId);
};
};
这个例子中,timeoutScheduler
Middleware 如果发现传入的 action
参数带有 meta.delay
字段,那么就认为这个 action
需要延时发送。当声明的延迟时间(meta.delay
)到了,action
对象才会被送往下一个 Middleware 的 dispatch
方法。
下面这个 Middleware 非常简单,但是却提供了非常灵活的用法。
Thunk
如果不了解 Thunk 的概念,可以先阅读 http://www.ruanyifeng.com/blog/2015/05/thunk.html 。
thunk
Middleware 的实现非常简单:
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action);
下面的例子装载了 thunk
,且 dispatch
了一个 Thunk 函数作为 action
。
const createStoreWithMiddleware = applyMiddleware(
logger,
thunk
timeoutScheduler
)(createStore);
const store = createStoreWithMiddleware(combineReducers(reducers));
function addFave(tweetId) {
return (dispatch, getState) => {
if (getState.tweets[tweetId] && getState.tweets[tweetId].faved)
return;
dispatch({type: IS_LOADING});
// Yay, that could be sync or async dispatching
remote.addFave(tweetId).then(
(res) => { dispatch({type: ADD_FAVE_SUCCEED}) },
(err) => { dispatch({type: ADD_FAVE_FAILED, err: err}) },
};
}
store.dispatch(addFave());
这个例子演示了 “收藏” 一条微博的相关的 action
对象的产生过程。addFave
作为 Action Creator
,返回的不是 Javascript Plain Object,而是一个接收 dispatch
和 getState
作为参数的 Thunk 函数。
当 thunk
Middleware 发现传入的 action
是这样的 Thunk 函数时,就会为该函数配齐 dispatch
和 getState
参数,让 Thunk 函数得以执行,否则,就调用 next(action)
让后续 Middleware 获得 dispatch
的机会。
在 Thunk 函数中,首先会判断当前应用的 state
中的微博是否已经被 fave 过了,如果没有,才会调用远程方法。
如果需要调用远程方法的话,那么首先发出 IS_LOADING
action
,告诉 关心这个状态的reducer
一个远程调用启动了。从而让 reducer
可以更新对应的 state
属性。这样关心此状态的 UI Component
则可以据此更新界面提示信息。
远程方法如果调用成功,就会 dispatch
代表成功的 action
对象({type: ADD_FAVE_SUCCEED}
),否则,产生的就是代表失败的 action
对象({type: ADD_FAVE_FAILED, err: err}
),自然会有关心这两个 action
的 reducer
来据此更新状态。无论如何,reducer
最后收到的 action
对象一定是这种 Javascript Plain Object。
当 Thunk Middleware 处理了 Thunk 函数类型的 action
之后,如果有配置了其他后续 Middlewares, 则将被跳过去而没有机会执行。
例如:我们的 Middlewares 配置为 applyMiddleware(logger, thunk, timeoutScheduler)
,当 action
是 Thunk 函数时,这个 action
将没有机会被 timeoutScheduler
Middleware 执行,而 logger
Middleware 则有机会在 thunk
Middleware 之前执行。每个 Middleware 自己决定给不给后续 Middleware 处理的机会。
applyMiddleware
拼装 Middlewares 的工具函数是 applyMiddleware
,该函数的模拟实现如下:
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();
let next = store.dispatch;
middlewares.forEach(middleware =>
next = middleware(store)(next)
);
return Object.assign({}, store, { dispatch: next });
}
结合 Middleware 的写法:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
我们可以看到,给 Middleware 传入 store
和 next
之后,返回的是一个新的 dispatch
方法。而传入的 next
参数则是之前 Middleware 返回的 dispatc
h 函数。这样,在真正传入 action
之前,我们得到了一个串联在一起的 dispatch
函数,该函数用来替代原本的store.dispatch
方法(通过 Object.assign(...)
)。Redux Middleware 机制的目的,就是以插件形式改变 store.dispatch
的行为方式,从而能够处理不同类型的 action
输入,得到最终的 Javascript Plain Object 形式的 action
对象。
每一个 Middleware 可以得到:
- 最初的
store
对象 (dispatch
属性还是原来的),因此,可以通过store.getState
获得最近的状态,以及通过原本的dispatch
对象直接发布action
对象,跳过其他 Middlewaredispatch
方法(next
)。上面vanillaPromise
演示了这样的用法。 -
next
方法: 前一个Middleware 返回的dispatch
方法。当前 Middleware 可以根据自己对action
的判断和处理结果,决定是否调用next
方法,以及传入什么样的参数。
以 newStore = applyMiddleware(logger,thunk,timeoutScheduler)(store))
这样的声明为例,timeoutScheduler
得到的next
参数就是原始的 store.dispatch
方法;thunk
拥有 timeoutScheduler
返回的 dispatch
方法,而 logger
又拥有 thunk
返回的 dispatch
方法。最后新生成的 newStore
的 dispatch
方法则是 logger
返回的。因此实际的 action
流动的顺序先到 logger
返回的 dispatch
方法,再到 thunk
返回的 dispatch
方法,最后到 timeoutScheduler
返回的 dispatch
方法。
需要注意一点, logger
因为排在 dispatch
链条的第一个,因此可以获得进入的每一个 action
对象。但是由于其他 Middleware 有可能异步调用 dispatch
(异步调用前一个 Middleware 返回的 dispatch
方法或者原始的 store.dispatch
),因此,logger
并一定有机会知道 action
最终是怎么传递的。
Middleware 可以有很多玩法的,下面文档列出了 Middleware 的原理和七种Middlewares:http://rackt.github.io/redux/docs/advanced/Middleware.html 。
store/reducer
是 Redux 的最核心逻辑,而 Middleware 是其外围的一种扩展方式,仅负责 action
对象的产生。但是由于 Redux 对于核心部分的限定非常严格(保持核心概念的简单):例如,reducer 必须是同步的,实际工程需求所带来的需求都被推到了 Dispatch/Middleware 这部分,官方文档提到的使用方式则起到了”最佳实践”的指导作用。
Higher-Order Store
Middleware 是对 store.dispatch
方法的扩展机制。但有些时候则需要对整个 store
对象都进行扩充,这就引入了 Higher-Order Store 的概念。
这个概念和 React 的 Higher-Order Component 概念是类似的。https://github.com/gaearon/redux/blob/cdaa3e81ffdf49e25ce39eeed37affc8f0c590f7/docs/higher-order-stores.md ,既提供一个函数,接受 store
对象作为输入参数,产生一个新的 store
对象作为返回值。
createStore => createStore'
Redux 建议大家在 Middleware 不能满足扩展要求的前提下再使用 Higher-Order Store,与 Redux 配套的 redux-devtools 就是一个例子。
Binding To React (React-Native)
上面的章节介绍了 Redux 的核心组组件和数据流程,可以通过下图回味一下:
┌──────────────┐
┌─────────────┐ ┌──▶│ subReducer 1 │
┌───▶│Middleware 1 │ │ └──────────────┘
│ └─────────────┘ │ │
│ │ │ ▼
┌─────────────┐ │ │ ┌───────────────┐ ┌──────────┐ │ ┌──────────────┐
│ action' │────┘ ▼ ┌──▶│store.dispatch │───▶│ reducer │───┘ │ subReducer m │
└─────────────┘ ┌─────────────┐ │ └───────────────┘ └──────────┘ └──────────────┘
│Middleware n │ │ │
└─────────────┘ │ │
│ │ ▼
│ │ ┌──────────────┐
└──────────┘ │ state │
plain action └──────────────┘
Redux 解决的是应用程序状态存储以及如何变更的问题,至于怎么用,则依赖于其他模块。关于如何在 React 或者 React-Native 中使用 Redux ,则需要参考 react-redux。
react-redux 是 React Components 如何使用 Redux 的 Binding。下面我们来分析一个具体的例子。
import { Component } from 'react';
export default class Counter extends Component {
render() {
return (
);
}
}
这是一个 React Component,显示了一个按钮。按下这个按钮,就会调用 this.props.onIncrement
。onIncrement
的具体内容在下面的例子中, 起作用为每次调用 onIncrement
就会 dispatch
{type: INCREMENT}
Action 对象来更新 Store/State
。
在 react-redux 中,这样的 Component 被称为 “Dumb” Component,既其本身对 Redux 完全无知,它只知道从 this.props
获取需要的 Action Creator
并且了解其语义,适当的时候调用该方法。而 “Dumb” Component 需要展现的外部数据也来自于 this.props
。
如何为 “Dumb” Component 准备 this.props
呢?react-redux 提供的 connect
函数帮助你完成这个功能:
import { Component } from 'react';
import { connect } from 'react-redux';
import Counter from '../components/Counter';
import { increment } from '../actionsCreators';
// Which part of the Redux global state does our component want to receive as props?
function mapStateToProps(state) {
return {
value: state.counter
};
}
// Which action creators does it want to receive by props?
function mapDispatchToProps(dispatch) {
return {
onIncrement: () => dispatch(increment())
};
}
export default connect( // Line 20
mapStateToProps,
mapDispatchToProps
)(Counter);
第 20 行的 connect
将 state
的某个(些)属性映射到了 Counter
Component 的 this.props
属性中,同时也把针对特定的Action Creator
的 dispatch
方法传递给了 this.props
。这样在 Counter
Component 中仅仅通过 this.props
就可以完成 action dispatching 和 应用程序状态获取的动作。
如果 connect 函数省掉第二个参数,connect(mapStateToProps)(Counter)
,那么 dispatch
方法会被直接传递给 this.props
。这不是推荐的方式,因为这意味着 Counter
需要了解 dispatch
的功能和语义了。
Components 的嵌套
你可以在你的组件树的任何一个层次调用 connect
来为下层组件绑定状态和 dispatch
方法。但是仅在你的顶层组件调用 connect
进行绑定是首选的方法。
Provider Component
上面的例子实际上是不可执行的,因为 connect
函数其实并没有 Redux store
对象在哪里。所以我们需要有一个机制让 connect
知道从你那里获得 store
对象,这是通过 Provider
Component 来设定的,Provider Component
也是 react-redux 提供的工具组件。
React.render(
{() => }
,
rootEl
);
Provider
Component 应该是你的 React Components 树的根组件。由于 React 0.13 版本的问题,Provider
Component 的子组件必须是一个函数,这个问题将在 React 0.14 中修复。
Provider
Component 和 connect
函数的配合,使得 React Component 在对 Redux 完全无感的情况下,仅通过 React 自身的机制来获取和维护应用程序的状态。
selector
在上面的例子中,connect(mapStateToProps,mapDispatchToProps)(Counter)
中的 mapStateToProps
函数通过返回一个映射对象,指定了哪些 Store/State
属性被映射到 React Component 的 this.props
,这个方法被称为 selector
。selector
的作用就是为 React Components 构造适合自己需要的状态视图。selector
的引入,降低了 React Component 对 Store/State
数据结构的依赖,利于代码解耦;同时由于 selector
的实现完全是自定义函数,因此也有足够的灵活性(例如对原始状态数据进行过滤、汇总等)。
reselect 这个项目提供了带 cache 功能的 selector
。如果 Store/State
和构造 view 的参数没有变化,那么每次 Component 获取的数据都将来自于上次调用/计算的结果。得益于 Store/State
Immutable 的本质,状态变化的检测是非常高效的。
总结
- Redux 和 React 没有直接关系,它瞄准的目标是应用状态管理。
- 核心概念是 Map/Reduce 中的 Reduce。且
Reducer
的执行是同步,产生的State
是 Immutable 的。 - 改变
State
只能通过向 Reducer dispatch actions 来完成。 -
State
的不同字段,可以通过不同的Reducers
来分别维护。combineReducers
负责组合这些Reducers
,前提是每个Reducer
只能维护自己关心的字段。 -
Action
对象只能是 Javascript Plain Object,但是通过在store
上装载middleware
,则可以任意定义action
对象的形式,反正会有特定的middleware
负责将此action
对象变为 Javascript Plain Object。可以以middleware
链条为集中点实现很多控制逻辑,例如 Log,Undo, ErrorHandler 等。 - Redux 仅仅专注于应用状态的维护,
reducer
、dispatch/middleware
是两个常用扩展点、Higher-order Store 则仅针对需要扩展全部Store
功能时使用。 - react-redux 是 Redux 针对 React/React-Native 的 Binding,
connect/selector
是扩展点,负责将store
中的状态添加到 Reactcomponent
的props
中。 - Redux 借用了很多函数式编程的思想,了解函数式编程会利于理解其实现原理,虽然使用它不需要了解很多函数式编程的概念。和 Flux 相比,Redux 的概念更精简、约定更严格、状态更确定、而是扩展却更灵活。
- 通过 https://github.com/xgrommx/awesome-redux 可以获得大量参考。
其他参考
大而全的所有 Redux 参考资料。
https://github.com/xgrommx/awesome-redux
Slack 讨论组
加入 https://reactiflux.slack.com Team,然后选择 redux channel。