前言
生活还是对我下手了,react 全家桶一个都不能少,这次是 redux。
redux介绍
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。
redux 有三大原则:单一数据源,State 是只读的,使用纯函数来执行修改
几个概念
action
唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的‘普通对象’。
这样确保了视图和网络请求都不能直接修改 state
,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition
的出现。 action
就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。
1 | { |
这就是一个 action
。我们约定,action
内必须使用一个字符串类型的 type
字段来表示将要执行的动作。除了 type
字段外,action
对象的结构完全由你自己决定。
action creator
因为一个应用有很多 action
,不可能自己写死,所以我们用一个函数来生成 action
,其实这个函数只是返回一个对象,这个对象就是 action
:1
2
3
4
5
6function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store
的,记住 actions
只是描述了有事情发生了这一事实,并没有描述应用如何更新 state
。reducer
就是一个纯函数,接收旧的 state
和 action
,返回新的 state
。1
(previousState, action) => newState
保持 reducer
纯净非常重要。永远不要在 reducer
里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如 Date.now() 或 Math.random()。
一个完整的 reducer
:1
2
3
4
5
6
7
8
9
10function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
根据传进来的 action
里面的 type
来执行不同的操作,然后返回一个新的 state
,注意:不要修改 state。在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。
当然,当应用越来越大,需要对 reducer
进行拆分。Redux提出通过定义多个reducer
对数据进行拆解访问或者修改,最终再通过combineReducers
函数将零散的数据拼装回去:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Function names don't matter!
function processA(state, action) { ... }
function doSomethingWithB(state, action) { ... }
let reducer = combineReducers({
a: processA,
b: doSomethingWithB
});
/* 这种方法等价于 combineReducers
function reducer(state = {}, action) {
return {
a: processA(state.a, action),
b: doSomethingWithB(state.b, action)
};
}
*/
let store = createStore(reducer);
每一个子reducer
都将直接对应数据源(store
)的某一个字段,这样将所有的 reducer
组合之后,就形成一棵状态树:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import { combineReducers } from 'redux';
// 叶子reducer
function aReducer(state = 1, action) {/*...*/}
function cReducer(state = true, action) {/*...*/}
function eReducer(state = [2, 3], action) {/*...*/}
const dReducer = combineReducers({
e: eReducer
});
const bReducer = combineReducers({
c: cReducer,
d: dReducer
});
// 根reducer
const rootReducer = combineReducers({
a: aReducer,
b: bReducer
});
store
redux 的 store
将整个应用的 state 储存在一棵 object tree
中,并且这个 object tree
只存在于唯一一个 store 中。简单来说,store
就是把 action
和 reducer
连接起来。1
2
3
4import { createStore } from 'redux'
import todoApp from './reducers' // 合成的 reducer
// 用 createStore 生成 store
let store = createStore(todoApp)
store
有几个方法
getState()
方法获取 state;dispatch(action)
方法更新 state;subscribe(listener)
注册监听器;
react 和 redux 结合
上面那些全都是 redux 的内容,完全可以不用 react 就能跑起来。用 react 写的展示组件和用 redux 写的容器组件 通过 connect()
函数结合起来。
使用 connect()
前,需要先定义 mapStateToProps
这个函数来指定如何把当前 Redux store state
映射到展示组件的 props
中。其实就是从完整的 store
树里面抽出这个展示组件需要的一些 state
,这样这个组件就不会感知到其他 state
的存在。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps
始终返回一个对象。
除了读取 state
,容器组件还能分发 action
。类似的方式,可以定义 mapDispatchToProps()
方法接收 dispatch()
方法并返回期望注入到展示组件的 props
中的回调方法。1
2
3
4
5
6
7const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
mapDispatchToProps
始终返回一个对象。
最后,使用 connect()
创建 VisibleTodoList
,并传入这两个函数。1
2
3
4
5
6
7
8import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
当用 connect()
连接后,我们在 APP
组件都是用这个连接之后的组件了。1
2
3
4
5
6
7
8
9import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<VisibleTodoList />
</div>
)
export default App
最后一步,传入 store
使用指定的 React Redux 组件 <Provider>
来 魔法般的 让所有容器组件都可以访问 store
,而不必显示地传递它。只需要在渲染根组件时使用即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
最后用一张图来总结:
异步
之前的都是我们的 action
都是同步操作,但是我们的网络请求或者其他需要异步的操作,单纯的 redux 并不能提供异步的操作,所以又需要其他库(惊不惊喜),这里介绍一下算是用的比较多的库:redux-thunk 和 redux-saga,这些库都是用到 redux midlleware 的特性,简单来说就是它提供的是位于 action
被发起之后,到达 reducer
之前的扩展点。你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
redux-thunk
这个库是官网里面作为教程的,所以也有一定的意义。通过使用指定的 middleware
,action 创建函数
除了返回 action
对象外还可以返回函数。这时,这个 action
创建函数就成为了 thunk。当 action 创建函数
返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action
,就像 dispatch
前面定义的同步 action
一样。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。
// 这时 dispatch 的是一个同步的 action
dispatch(requestPosts(subreddit))
// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。
// 这个案例中,我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因为会捕获
// 在 dispatch 和渲染中出现的任何错误,
// 导致 'Unexpected batch number' 错误。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。
// 这个也是一个同步的 action
dispatch(receivePosts(subreddit, json))
)
}
}
然后,我们在在 dispatch 机制中引入 Redux Thunk middleware ,我们使用了 applyMiddleware()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'
// 另外一个 middleware
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允许我们 dispatch() 函数
loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
)
)
store.dispatch(selectSubreddit('reactjs'))
store
.dispatch(fetchPosts('reactjs'))
.then(() => console.log(store.getState())
)
这个异步 action 创建函数还可以有另外一个参数 getState
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18export function fetchPostsIfNeeded(subreddit) {
// 注意这个函数也接收了 getState() 方法
// 它让你选择接下来 dispatch 什么。
// 当缓存的值是可用时,
// 减少网络请求很有用。
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
// 在 thunk 里 dispatch 另一个 thunk!
return dispatch(fetchPosts(subreddit))
} else {
// 告诉调用代码不需要再等待。
return Promise.resolve()
}
}
}
最后
关于 redux-saga ,由于现在接触的项目没有怎么用,也不好说,所以等实际使用之后再来记录吧。