# 4.组件状态管理,Redux、Mobx
# 基本概念和 API
# 1. Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Redux 提供 createStore 这个函数,用来生成 Store。
import { createStore } from "redux";
const store = createStore(fn);
2
# 1. State
Store 对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过 store.getState()拿到。
import { createStore } from "redux";
const store = createStore(fn);
const state = store.getState();
2
3
4
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
# 3. Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的 type 属性是必须的,表示 Action 的名称。其他属性可以自由设置,社区有一个规范可以参考。
const action = {
type: "ADD_TODO",
payload: "Learn Redux",
};
2
3
4
可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
# 4. store.dispatch()
store.dispatch()是 View 发出 Action 的唯一方法。
import { createStore } from "redux";
const store = createStore(fn);
store.dispatch({
type: "ADD_TODO",
payload: "Learn Redux",
});
2
3
4
5
6
7
上面代码中,store.dispatch 接受一个 Action 对象作为参数,将它发送出去。
# Reducer
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
const reducer = function (state, action) {
// ...
return new_state;
};
2
3
4
整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case "ADD":
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: "ADD",
payload: 2,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,reducer 函数收到名为 ADD 的 Action 以后,就返回一个新的 State,作为加法的计算结果。其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。
实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch 方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入 createStore 方法。
import { createStore } from "redux";
const store = createStore(reducer);
2
上面代码中,createStore 接受 Reducer 作为参数,生成一个新的 Store。以后每当 store.dispatch 发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
为什么这个函数叫做 Reducer 呢?因为它可以作为数组的 reduce 方法的参数。请看下面的例子,一系列 Action 对象按照顺序作为一个数组。
const actions = [
{ type: "ADD", payload: 0 },
{ type: "ADD", payload: 1 },
{ type: "ADD", payload: 2 },
];
const total = actions.reduce(reducer, 0); // 3
2
3
4
5
6
7
上面代码中,数组 actions 表示依次有三个 Action,分别是加 0、加 1 和加 2。数组的 reduce 方法接受 Reducer 函数作为参数,就可以直接得到最终的状态 3。
# Reducer:纯函数
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。 由于 Reducer 是纯函数,就可以保证同样的 State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。
// State 是一个对象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}
// State 是一个数组
function reducer(state, action) {
return [...state, newItem];
}
2
3
4
5
6
7
8
9
10
11
最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。
# store.subscribe()
Store 允许使用 store.subscribe 方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from "redux";
const store = createStore(reducer);
store.subscribe(listener);
// 显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。
// store.subscribe方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() => console.log(store.getState()));
unsubscribe();
2
3
4
5
6
7
8
9
10
11
# Redux 的中间件和异步操作
# 中间件的概念
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
想来想去,只有发送 Action 的这个步骤,即 store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对 store.dispatch 进行如下改造。
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log("dispatching", action);
next(action);
console.log("next state", store.getState());
};
2
3
4
5
6
上面代码中,对 store.dispatch 进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对 store.dispatch 方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
常用的中间件都有现成的,只要引用别人写好的模块即可。比如,上一节的日志中间件,就有现成的 redux-logger 模块。
# 异步操作的基本思路
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
# 第一种解决方案-redux-thunk
写出一个返回函数的 Action Creator,然后使用 redux-thunk 中间件改造 store.dispatch,使得后者可以接受函数作为参数(store.dispatch 方法正常情况下,参数只能是对象,不能是函数。)
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(postTitle, json)));
};
};
// 使用方法一
store.dispatch(fetchPosts('reactjs'));
// 使用方法二
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
);
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,fetchPosts 是一个 Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个 Action(requestPosts(postTitle)),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json))。
# 第二种解决方案-redux-promise 中间件
既然 Action Creator 可以返回函数,当然也可以返回其他值。另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。
这时,Action Creator 有两种写法。写法一,返回值是一个 Promise 对象。
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
2
3
4
5
6
7
8
9
写法二,Action 对象的 payload 属性是一个 Promise 对象。这需要从 redux-actions 模块引入 createAction 方法,并且写法也要变成下面这样。
import { createAction } from 'redux-actions';
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出同步 Action
dispatch(requestPosts(selectedPost));
// 发出异步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction 的第二个参数必须是一个 Promise 对象。