redux
redux的设计思想来源于Flux,但是要比Flux的模型要更加的简化。
在redux中,数据流是单向流动的。在redux中有三个主要的概念
- Action
- Reducer
- Store
在redux中,也有三大原则,这三原则其实是和之前提到的三个主要概念是对应的
- 单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- state是只读的
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 使用纯函数来执行修改
Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。
基础
Store
store
是应用状态 state
的管理者。
state
是应用的黄台,一般本质上是一个普通对象,比如一个计数器的计数次数这样的对象属性。
store 包含下列四个函数:
- getState() 获取整个state
- dispatch(action) 出发state 改变的 【唯一途径】
- subscribe(listener) 在store中添加监听
- replaceReducer(nextReducer) 一般是在 Webpack Code-Splitting 按需加载的时候用
store和state的关系是 state = store.getState()
Redux 规定,一个应用只应有一个单一的 store , 其管理着唯一的应用状态 state Redux 还规定,不能直接修改应用的状态 state , 也就是说,下面的行为是不允许的
var state = store.getState()
state.counter = state.counter + 1 // 禁止在业务逻辑中直接修改 state
要改变 state
,正确的做法是 dispatch
一个action
。
store是通过调用Redux中的 createStore
函数生成的:
import { createStore } from 'redux'
...
const store = createStore(reducer, initialState) // store 是靠传入 reducer 生成的哦!
现在您只需要记住 reducer 是一个 函数,负责更新并返回一个新的 state而 initialState 主要用于前后端同构的数据同步(详情请关注 React 服务端渲染)
Action
% action
Action 是把数据从应用或者视图传到store的有效负载。他是store 数据的唯一来源。
一般来说会通过store.dispatch()
函数将action传到store。
action 只是一个包含 type 属性的普通对象。例如 { type: 'INCREMENT' }
。这个 type 是我们实现用户轻微追踪的关键,一个更复杂的例子:
{
type: 'ADD_TODO',
payload: {
id: 1,
content: '待办事项1',
completed: false
}
}
当然,action
的形式是多种多样的,唯一的约束就是必须包含一个 type
属性
以下 action
都是合法的:
// 如下都是合法的,但就是不够规范
{
type: 'ADD_TODO',
id: 1,
content: '待办事项1',
completed: false
}
{
type: 'ADD_TODO',
abcdefg: {
id: 1,
content: '待办事项1',
completed: false
}
}
规范如下
规则
一个action必须的属性:
- 一定是一个普通的JavaScript对象
- 这个对象有type属性
一个action可能拥有的属性:
error
属性payload
属性meta
属性一个action不要有除了type、payload、meta、meta之外的属性
type
是字符串类型的数据,用于识别一个action
payload
可选的属性payload属性可以是任何类型的值。它代表了action的有效负载。不是action的type或者状态的信息的内容都应该包含在
payload
中。 举个例子,如果error字段的属性是true,那么payload的内容就应该是错误对象。error
这个字段是布尔类型,这个字段是true的时候,那么payload就是error对象 error是null或者undefined的时候,action也不是error
meta
其他非负载的属性都可以存在meta中
& Action Creator
Action Creator 可以是同步的,也可以是异步的。
action创建函数就是生成action的方法。 “action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
// 本代码块记为 code-4
var id = 1function addTodo(content) {
return {
type: 'ADD_TODO',
payload: {
id: id++,
content: content, // 待办事项内容
completed: false // 是否完成的标识
}
}
}
然后再看看实际应用
<--! 本代码块记为 code-5 -->
<input type="text" id="todoInput" />
<button id="btn">提交</button>
<script>
$('#btn').on('click', function() {
var content = $('#todoInput').val() // 获取输入框的值
var action = addTodo(content) // 执行 Action Creator 获得 action
store.dispatch(action) // 改变 state 的不二法门:dispatch 一个 action!!!
})
</script>
action
虽然没有强制的规范,但是在 store.dispatch(action)
之后,Redux 会明确知道是提取 action.payload
,并且是对应写入到 state。todos
数组中,这都是 Reducer 的功劳。
Reducer
Reducer 必须是同步的纯函数
reducer 的实质是一个纯函数,reducers 指定了应用状态的变化如何响应 actions 并发送到 store,actions 只是描述了 有事情发生 这个试试,并没有描述应用应该如何更新 state 。而描述如何更新 store 是reducer的工作。
reducer会更具 action.type
来更新 state
并返回 nextState
最后会用 reducer
的返回值 nextState
完全替换掉 原来的 state
var initState = {
counter: 0,
todos: []
}
function reducer(state, action) {
// ※ 应用的初始状态是在第一次执行 reducer 时设置的 ※
if (!state) state = initState
switch (action.type) {
case 'ADD_TODO':
var nextState = _.cloneDeep(state) // 用到了 lodash 的深克隆
nextState.todos.push(action.payload)
return nextState
default:
// 由于 nextState 会把原 state 整个替换掉
// 若无修改,必须返回原 state(否则就是 undefined)
return state
}
}
§ 总结
store
由 Redux 的createStore(reducer)
生成state
通过store.getState()
获取,本质上一般是一个存储着整个应用状态的对象action
本质上是一个包含type
属性的普通对象,由 Action Creator (函数) 产生- 改变
state
必须dispatch
一个action
reducer
本质上是根据action.type
来更新state
并返回nextState
的函数reducer
必须返回值,否则nextState
即为undefined
- 实际上,
state
就是所有reducer
返回值的汇总(本教程只有一个reducer
,主要是应用场景比较简单)
Action Creator =>
action
=>store.dispatch(action)
=>reducer(state, action)
=>原 state
state = nextState
⊙ Redux 与传统后端 MVC 的对照
Redux | 传统后端 MVC
—|—
store
| 数据库实例
state
| 数据库中存储的数据
dispatch(action)
| 用户发起请求
action: { type, payload }
| type
表示请求的 URL,payload
表示请求的数据
reducer
| 路由 + 控制器(handler)
reducer
中的 switch-case
分支 | 路由,根据 action.type
路由到对应的控制器
reducer
内部对 state
的处理 | 控制器对数据库进行增删改操作
reducer
返回 nextState
| 将修改后的记录写回数据库
进阶
Redux API
API 汇总
Redux 有五个 API,分别是:
createStore(reducer, [initialState])
combineReducers(reducers)
applyMiddleware(...middlewares)
bindActionCreators(actionCreators, dispatch)
compose(...functions)
createStore
生成的 store
有四个 API,分别是:
getState()
dispatch(action)
subscribe(listener)
replaceReducer(nextReducer)
§ [compose(…functions)][compose]
组合函数
<!DOCTYPE html>
<html>
<head>
<script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
</head>
<body>
<script>
function func1(num) {
console.log('func1 获得参数 ' + num);
return num + 1;
}
function func2(num) {
console.log('func2 获得参数 ' + num);
return num + 2;
}
function func3(num) {
console.log('func3 获得参数 ' + num);
return num + 3;
}
// 有点难看(如果函数名再长一点,那屏幕就不够宽了)
var re1 = func3(func2(func1(0)));
console.log('re1:' + re1);
console.log('===============');
// 很优雅
var re2 = Redux.compose(func3, func2, func1)(0);
console.log('re2:' + re2);
</script>
</body>
</html>
控制台输出
func1 获得参数 0
func2 获得参数 1
func3 获得参数 3
re1:6
===============
func1 获得参数 0
func2 获得参数 1
func3 获得参数 3
re2:6
§ createStore(reducer, initialState, enhancer)
- reducer
- initialState,主要用于前后端同构时的数据同步
- enhancer 很牛逼,可以实现中间件、时间旅行,持久化等
§ combineReducers(reducers)
§ bindActionCreators(actionCreators, dispatch)
var actionsCreators = Redux.bindActionCreators(
{ addTodo: addTodo },
store.dispatch // 传入 dispatch 函数
)
$('#btn').on('click', function() {
var content = $('#todoInput').val()
actionCreators.addTodo(content) // 它会自动 dispatch
})d
§ applyMiddleware(…middlewares)
<!DOCTYPE html>
<html>
<head>
<script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script>
</head>
<body>
<script>
/** Action Creators */
function inc() {
return { type: 'INCREMENT' };
}
function dec() {
return { type: 'DECREMENT' };
}
function reducer(state, action) {
state = state || { counter: 0 };
switch (action.type) {
case 'INCREMENT':
return { counter: state.counter + 1 };
case 'DECREMENT':
return { counter: state.counter - 1 };
default:
return state;
}
}
function printStateMiddleware(middlewareAPI) {
return function (dispatch) {
return function (action) {
console.log('dispatch 前:', middlewareAPI.getState());
var returnValue = dispatch(action);
console.log('dispatch 后:', middlewareAPI.getState(), '\n');
return returnValue;
};
};
}
var enhancedCreateStore = Redux.applyMiddleware(printStateMiddleware)(Redux.createStore);
var store = enhancedCreateStore(reducer);
store.dispatch(inc());
store.dispatch(inc());
store.dispatch(dec());
</script>
</body>
</html>
控制台输出:
dispatch 前:{ counter: 0 }
dispatch 后:{ counter: 1 }
dispatch 前:{ counter: 1 }
dispatch 后:{ counter: 2 }
dispatch 前:{ counter: 2 }
dispatch 后:{ counter: 1 }
实际上,上面生成 store
的代码可以更加优雅:
/** 本代码块记为 code-10 **/
var store = Redux.createStore(
reducer,
Redux.applyMiddleware(printStateMiddleware)
)
如果有多个中间件以及多个增强器,还可以这样写(请留意序号顺序):
重温一下
createStore
完整的函数签名:function createStore(reducer, preloadedState, enhancer)
/** 本代码块记为 code-11 **/
import { createStore, applyMiddleware, compose } from 'redux'
const store = createStore(
reducer,
preloadedState, // 可选,前后端同构的数据同步
compose( // 还记得吗?compose 是从右到左的哦!
applyMiddleware( // 这货也是 Store Enhancer 哦!但这是关乎中间件的增强器,必须置于 compose 执行链的最后
middleware1,
middleware2,
middleware3
),
enhancer3,
enhancer2,
enhancer1
)
)
为什么会支持那么多种写法呢?在 createStore
的源码分析的开头部分,我省略了一些代码,现在奉上该压轴部分:
/** 本代码块记为 code-12 **/
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
// 这里就是上面 code-10 的情况,只传入 reducer 和 Store Enhancer 这两个参数
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
// 存在 enhancer 就立即执行,返回增强版的 createStore <--------- 记为【锚点 12-1】
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
// 除 compose 外,createStore 竟然也在此为我们提供了书写的便利与自由度,实在是太体贴了
react-redux
import React, { PropTypes } from 'react';
import { Provider } from 'react-redux';
import { Router, Route, browserHistory } from 'react-router';
import App from './App';
const Root = ({ store }) => (
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/(:filter)" component={App} />
</Router>
</Provider>);
Root.propTypes = {
store: PropTypes.object.isRequired,};
export default Root;