# 4.组件状态管理,Redux、Mobx

# 基本概念和 API

# 1. Store

Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。

Redux 提供 createStore 这个函数,用来生成 Store。

import { createStore } from "redux";
const store = createStore(fn);
1
2

# 1. State

Store 对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。

当前时刻的 State,可以通过 store.getState()拿到。

import { createStore } from "redux";
const store = createStore(fn);

const state = store.getState();
1
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",
};
1
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",
});
1
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;
};
1
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,
});
1
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);
1
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
1
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];
}
1
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();
1
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());
};
1
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())
);
1
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()
       });
});
1
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())
    ));
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,第二个 dispatch 方法发出的是异步 Action,只有等到操作结束,这个 Action 才会实际发出。注意,createAction 的第二个参数必须是一个 Promise 对象。

Last Updated: 6/3/2024, 1:08:34 AM